Skip to content

Commit 3c60dc3

Browse files
fix(site): show error on duplicate template rename attempt (#15348)
Fixes #15311.
1 parent 2d00b50 commit 3c60dc3

File tree

4 files changed

+70
-3
lines changed

4 files changed

+70
-3
lines changed

coderd/templates.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -841,7 +841,17 @@ func (api *API) patchTemplateMeta(rw http.ResponseWriter, r *http.Request) {
841841
return nil
842842
}, nil)
843843
if err != nil {
844-
httpapi.InternalServerError(rw, err)
844+
if database.IsUniqueViolation(err, database.UniqueTemplatesOrganizationIDNameIndex) {
845+
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
846+
Message: fmt.Sprintf("Template with name %q already exists.", req.Name),
847+
Validations: []codersdk.ValidationError{{
848+
Field: "name",
849+
Detail: "This value is already in use and should be unique.",
850+
}},
851+
})
852+
} else {
853+
httpapi.InternalServerError(rw, err)
854+
}
845855
return
846856
}
847857

coderd/templates_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/coder/coder/v2/coderd/coderdtest"
1919
"github.com/coder/coder/v2/coderd/database"
2020
"github.com/coder/coder/v2/coderd/database/dbauthz"
21+
"github.com/coder/coder/v2/coderd/database/dbtestutil"
2122
"github.com/coder/coder/v2/coderd/database/dbtime"
2223
"github.com/coder/coder/v2/coderd/notifications"
2324
"github.com/coder/coder/v2/coderd/rbac"
@@ -612,6 +613,32 @@ func TestPatchTemplateMeta(t *testing.T) {
612613
assert.Equal(t, database.AuditActionWrite, auditor.AuditLogs()[4].Action)
613614
})
614615

616+
t.Run("AlreadyExists", func(t *testing.T) {
617+
t.Parallel()
618+
619+
if !dbtestutil.WillUsePostgres() {
620+
t.Skip("This test requires Postgres constraints")
621+
}
622+
623+
ownerClient := coderdtest.New(t, nil)
624+
owner := coderdtest.CreateFirstUser(t, ownerClient)
625+
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.ScopedRoleOrgTemplateAdmin(owner.OrganizationID))
626+
627+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
628+
version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
629+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
630+
template2 := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version2.ID)
631+
632+
ctx := testutil.Context(t, testutil.WaitLong)
633+
634+
_, err := client.UpdateTemplateMeta(ctx, template.ID, codersdk.UpdateTemplateMeta{
635+
Name: template2.Name,
636+
})
637+
var apiErr *codersdk.Error
638+
require.ErrorAs(t, err, &apiErr)
639+
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
640+
})
641+
615642
t.Run("AGPL_Deprecated", func(t *testing.T) {
616643
t.Parallel()
617644

site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.test.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { API, withDefaultFeatures } from "api/api";
44
import type { Template, UpdateTemplateMeta } from "api/typesGenerated";
55
import { Language as FooterFormLanguage } from "components/FormFooter/FormFooter";
66
import { http, HttpResponse } from "msw";
7-
import { MockEntitlements, MockTemplate } from "testHelpers/entities";
7+
import {
8+
MockEntitlements,
9+
MockTemplate,
10+
mockApiError,
11+
} from "testHelpers/entities";
812
import {
913
renderWithTemplateSettingsLayout,
1014
waitForLoaderToBeRemoved,
@@ -112,6 +116,28 @@ describe("TemplateSettingsPage", () => {
112116
await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1));
113117
});
114118

119+
it("displays an error if the name is taken", async () => {
120+
await renderTemplateSettingsPage();
121+
jest.spyOn(API, "updateTemplateMeta").mockRejectedValueOnce(
122+
mockApiError({
123+
message: `Template with name "test-template" already exists`,
124+
validations: [
125+
{
126+
field: "name",
127+
detail: "This value is already in use and should be unique.",
128+
},
129+
],
130+
}),
131+
);
132+
await fillAndSubmitForm(validFormValues);
133+
await waitFor(() => expect(API.updateTemplateMeta).toBeCalledTimes(1));
134+
expect(
135+
await screen.findByText(
136+
"This value is already in use and should be unique.",
137+
),
138+
).toBeInTheDocument();
139+
});
140+
115141
it("allows a description of 128 chars", () => {
116142
const values: UpdateTemplateMeta = {
117143
...validFormValues,

site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsPage.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { API } from "api/api";
2+
import { getErrorMessage } from "api/errors";
23
import { templateByNameKey } from "api/queries/templates";
34
import type { UpdateTemplateMeta } from "api/typesGenerated";
4-
import { displaySuccess } from "components/GlobalSnackbar/utils";
5+
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
56
import { useDashboard } from "modules/dashboard/useDashboard";
67
import { linkToTemplate, useLinks } from "modules/navigation";
78
import type { FC } from "react";
@@ -51,6 +52,9 @@ export const TemplateSettingsPage: FC = () => {
5152
displaySuccess("Template updated successfully");
5253
navigate(getLink(linkToTemplate(data.organization_name, data.name)));
5354
},
55+
onError: (error) => {
56+
displayError(getErrorMessage(error, "Failed to update template"));
57+
},
5458
},
5559
);
5660

0 commit comments

Comments
 (0)