Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/api/internal/store/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -1220,7 +1220,10 @@ func normalizeWorkspaceSettings(current store.Workspace, input store.UpdateWorks
if workspaceSlug == "" {
return "", "", "", errors.New("workspace slug is required")
}
if isReservedWorkspaceSlug(workspaceSlug) {
// A workspace that already owns a reserved slug (the Access-provisioned
// default workspace) must stay editable: rejecting its unchanged slug
// would block every name and icon update from the settings form.
if workspaceSlug != current.Slug && isReservedWorkspaceSlug(workspaceSlug) {
return "", "", "", errors.New("workspace slug is reserved")
}
}
Expand Down
39 changes: 39 additions & 0 deletions apps/api/internal/store/sqlite/mutations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -700,3 +700,42 @@ func TestMutationsReturnOutboxErrors(t *testing.T) {
t.Fatal("expected message delete outbox error")
}
}

func TestUpdateWorkspaceKeepsReservedSlugEditable(t *testing.T) {
t.Parallel()
ctx := context.Background()
st := newTestStore(t)
owner, err := st.EnsureBootstrap(ctx, "Owner", "owner-reserved-slug@example.com")
if err != nil {
t.Fatal(err)
}
workspace, err := st.EnsureDefaultWorkspaceMember(ctx, owner.ID)
if err != nil {
t.Fatal(err)
}
if !isReservedWorkspaceSlug(workspace.Slug) {
t.Fatalf("expected the provisioned default workspace to own a reserved slug, got %q", workspace.Slug)
}
// The settings form always submits the current slug alongside the name.
name := "Renamed default"
sameSlug := workspace.Slug
updated, _, err := st.UpdateWorkspace(ctx, store.UpdateWorkspaceInput{WorkspaceID: workspace.ID, ActorUserID: owner.ID, Name: &name, Slug: &sameSlug})
if err != nil {
t.Fatalf("expected an unchanged reserved slug to stay editable: %v", err)
}
if updated.Name != name || updated.Slug != workspace.Slug {
t.Fatalf("unexpected workspace after rename: %#v", updated)
}
otherReserved := "guests"
if _, _, err := st.UpdateWorkspace(ctx, store.UpdateWorkspaceInput{WorkspaceID: workspace.ID, ActorUserID: owner.ID, Slug: &otherReserved}); err == nil {
t.Fatal("expected a move to another reserved slug to be rejected")
}
regular, err := st.CreateWorkspace(ctx, store.CreateWorkspaceInput{Name: "Regular", Slug: "regular"}, owner.ID)
if err != nil {
t.Fatal(err)
}
takeReserved := workspace.Slug
if _, _, err := st.UpdateWorkspace(ctx, store.UpdateWorkspaceInput{WorkspaceID: regular.ID, ActorUserID: owner.ID, Slug: &takeReserved}); err == nil {
t.Fatal("expected a regular workspace to be refused a reserved slug")
}
}
5 changes: 4 additions & 1 deletion apps/api/internal/store/sqlite/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -1232,7 +1232,10 @@ func normalizeWorkspaceSettings(current store.Workspace, input store.UpdateWorks
if workspaceSlug == "" {
return "", "", "", errors.New("workspace slug is required")
}
if isReservedWorkspaceSlug(workspaceSlug) {
// A workspace that already owns a reserved slug (the Access-provisioned
// default workspace) must stay editable: rejecting its unchanged slug
// would block every name and icon update from the settings form.
if workspaceSlug != current.Slug && isReservedWorkspaceSlug(workspaceSlug) {
return "", "", "", errors.New("workspace slug is reserved")
}
}
Expand Down
Loading