fix(auth): close cancellation and workspace races

This commit is contained in:
2026-08-17 19:32:03 +02:00
parent d12b629420
commit 0a2c667231
10 changed files with 822 additions and 221 deletions
@@ -62,6 +62,38 @@ function renderDeniedManager() {
);
}
type ManagerSettings = {
open: boolean;
canManageWorkspace: boolean;
canManageSecrets: boolean;
};
function renderManagerWithSettings(initial: Partial<ManagerSettings> = {}) {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
const onClose = vi.fn();
let settings: ManagerSettings = {
open: true,
canManageWorkspace: true,
canManageSecrets: true,
...initial,
};
const tree = () => (
<QueryClientProvider client={client}>
<WorkspaceManager onClose={onClose} {...settings} />
</QueryClientProvider>
);
const view = render(tree());
return {
...view,
client,
onClose,
rerender(next: Partial<ManagerSettings>) {
settings = { ...settings, ...next };
view.rerender(tree());
},
};
}
beforeEach(() => {
localStorage.clear();
setAuthState(authenticatedWorkspaceUser);
@@ -610,6 +642,377 @@ test("a delayed runtime-secret save from user A cannot repopulate user B's cache
expect(screen.queryByText("Runtime secrets saved. Stored values remain hidden.")).not.toBeInTheDocument();
});
test("a deferred secret save cannot update a reopened dialog", async () => {
const user = userEvent.setup();
let release!: () => void;
let started!: () => void;
let settled!: () => void;
const held = new Promise<void>((resolve) => { release = resolve; });
const requestStarted = new Promise<void>((resolve) => { started = resolve; });
const requestSettled = new Promise<void>((resolve) => { settled = resolve; });
server.use(http.put("/api/workspaces/psd-clinical/secrets", async () => {
started();
try {
await held;
return HttpResponse.json(runtimeConfiguration(true));
} finally {
settled();
}
}));
const manager = renderManagerWithSettings();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
await user.type(await screen.findByLabelText("Data warehouse password"), "one-time-password");
await user.click(screen.getByRole("button", { name: "Save entered secrets" }));
await requestStarted;
await user.click(screen.getByRole("button", { name: "Close workspace management" }));
manager.rerender({ open: false });
manager.rerender({ open: true });
await user.type(await screen.findByLabelText("Data warehouse password"), "new-secret");
expect(await screen.findByRole("button", { name: "Save entered secrets" })).not.toBeDisabled();
const cacheBeforeRelease = manager.client.getQueryData(["workspace-runtime-configuration", "psd-clinical"]);
act(() => release());
await act(async () => {
await requestSettled;
await new Promise((resolve) => { setTimeout(resolve, 50); });
});
expect(manager.client.getQueryData(["workspace-runtime-configuration", "psd-clinical"])).toEqual(cacheBeforeRelease);
expect(screen.queryByText("Runtime secrets saved. Stored values remain hidden.")).not.toBeInTheDocument();
});
test("a deferred secret save cannot update a workspace selected again after a cross-workspace transition", async () => {
const user = userEvent.setup();
const alternateId = "other-clinical";
const alternateWorkspace = canonicalWorkspaceFixture(alternateId);
const alternateRevision = workspaceRevisionFixture(alternateId);
let release!: () => void;
let started!: () => void;
let settled!: () => void;
const held = new Promise<void>((resolve) => { release = resolve; });
const requestStarted = new Promise<void>((resolve) => { started = resolve; });
const requestSettled = new Promise<void>((resolve) => { settled = resolve; });
server.use(
http.get("/api/workspaces", () => HttpResponse.json([
workspaceSummaryFixture("psd-clinical", {
displayName: "PSD Clinical", description: "Clinical data", configurationState: "configuration_required", revision,
}),
workspaceSummaryFixture(alternateId, {
displayName: "Other Clinical", description: "Other data", configurationState: "configuration_required", revision: alternateRevision,
}),
])),
http.get(`/api/workspaces/${alternateId}`, () => HttpResponse.json({ workspace: alternateWorkspace, revision: alternateRevision })),
http.get(`/api/workspaces/${alternateId}/runtime-configuration`, () => HttpResponse.json({
workspaceId: alternateId, revision: alternateRevision, configurationState: "configuration_required", requirements: [{ ...requirement }],
})),
http.put("/api/workspaces/psd-clinical/secrets", async () => {
started();
try {
await held;
return HttpResponse.json(runtimeConfiguration(true));
} finally {
settled();
}
}),
);
const manager = renderManagerWithSettings();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
await user.type(await screen.findByLabelText("Data warehouse password"), "one-time-password");
await user.click(screen.getByRole("button", { name: "Save entered secrets" }));
await requestStarted;
await user.click(screen.getByRole("button", { name: "Other Clinical" }));
expect(await screen.findByRole("heading", { name: "Other Clinical" })).toBeVisible();
await user.click(screen.getByRole("button", { name: "PSD Clinical" }));
await user.type(await screen.findByLabelText("Data warehouse password"), "new-secret");
expect(await screen.findByRole("button", { name: "Save entered secrets" })).not.toBeDisabled();
const cacheBeforeRelease = manager.client.getQueryData(["workspace-runtime-configuration", "psd-clinical"]);
act(() => release());
await act(async () => {
await requestSettled;
await new Promise((resolve) => { setTimeout(resolve, 50); });
});
expect(manager.client.getQueryData(["workspace-runtime-configuration", "psd-clinical"])).toEqual(cacheBeforeRelease);
expect(screen.queryByText("Runtime secrets saved. Stored values remain hidden.")).not.toBeInTheDocument();
});
test("a deferred secret save cannot update a context after its secret permission changes", async () => {
const user = userEvent.setup();
let release!: () => void;
let started!: () => void;
let settled!: () => void;
const held = new Promise<void>((resolve) => { release = resolve; });
const requestStarted = new Promise<void>((resolve) => { started = resolve; });
const requestSettled = new Promise<void>((resolve) => { settled = resolve; });
server.use(http.put("/api/workspaces/psd-clinical/secrets", async () => {
started();
try {
await held;
return HttpResponse.json(runtimeConfiguration(true));
} finally {
settled();
}
}));
const manager = renderManagerWithSettings();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
await user.type(await screen.findByLabelText("Data warehouse password"), "one-time-password");
await user.click(screen.getByRole("button", { name: "Save entered secrets" }));
await requestStarted;
manager.rerender({ canManageSecrets: false });
manager.rerender({ canManageSecrets: true });
await user.type(await screen.findByLabelText("Data warehouse password"), "new-secret");
expect(await screen.findByRole("button", { name: "Save entered secrets" })).not.toBeDisabled();
const cacheBeforeRelease = manager.client.getQueryData(["workspace-runtime-configuration", "psd-clinical"]);
act(() => release());
await act(async () => {
await requestSettled;
await new Promise((resolve) => { setTimeout(resolve, 50); });
});
expect(manager.client.getQueryData(["workspace-runtime-configuration", "psd-clinical"])).toEqual(cacheBeforeRelease);
expect(screen.queryByText("Runtime secrets saved. Stored values remain hidden.")).not.toBeInTheDocument();
});
test("a deferred repository update cannot update a reopened dialog or refetch its cache", async () => {
const user = userEvent.setup();
let release!: () => void;
let started!: () => void;
let settled!: () => void;
let statusRequests = 0;
let workspaceRequests = 0;
const held = new Promise<void>((resolve) => { release = resolve; });
const requestStarted = new Promise<void>((resolve) => { started = resolve; });
const requestSettled = new Promise<void>((resolve) => { settled = resolve; });
server.use(
http.get("/api/workspace-registry/status", () => {
statusRequests++;
return HttpResponse.json({ branch: "main", head: "a".repeat(40), ahead: 0, behind: 0, degraded: false });
}),
http.get("/api/workspaces", () => {
workspaceRequests++;
return HttpResponse.json([workspaceSummaryFixture("psd-clinical", {
displayName: "PSD Clinical", description: "Clinical data", configurationState: "configuration_required", revision,
})]);
}),
http.post("/api/workspace-registry/pull", async () => {
started();
try {
await held;
return HttpResponse.json({ branch: "main", head: "b".repeat(40), ahead: 0, behind: 0, degraded: false });
} finally {
settled();
}
}),
);
const manager = renderManagerWithSettings();
await user.click(await screen.findByRole("button", { name: "Update workspace repository" }));
await requestStarted;
await user.click(screen.getByRole("button", { name: "Close workspace management" }));
manager.rerender({ open: false });
manager.rerender({ open: true });
expect(await screen.findByRole("button", { name: "Update workspace repository" })).not.toBeDisabled();
const requestsBeforeRelease = { statusRequests, workspaceRequests };
act(() => release());
await act(async () => {
await requestSettled;
await new Promise((resolve) => { setTimeout(resolve, 50); });
});
expect({ statusRequests, workspaceRequests }).toEqual(requestsBeforeRelease);
expect(screen.queryByText("Workspace repository updated and validated.")).not.toBeInTheDocument();
});
test("a deferred repository update cannot update a context after a cross-workspace transition", async () => {
const user = userEvent.setup();
const alternateId = "other-clinical";
const alternateWorkspace = canonicalWorkspaceFixture(alternateId);
const alternateRevision = workspaceRevisionFixture(alternateId);
let release!: () => void;
let started!: () => void;
let settled!: () => void;
let statusRequests = 0;
let workspaceRequests = 0;
const held = new Promise<void>((resolve) => { release = resolve; });
const requestStarted = new Promise<void>((resolve) => { started = resolve; });
const requestSettled = new Promise<void>((resolve) => { settled = resolve; });
server.use(
http.get("/api/workspace-registry/status", () => {
statusRequests++;
return HttpResponse.json({ branch: "main", head: "a".repeat(40), ahead: 0, behind: 0, degraded: false });
}),
http.get("/api/workspaces", () => {
workspaceRequests++;
return HttpResponse.json([
workspaceSummaryFixture("psd-clinical", {
displayName: "PSD Clinical", description: "Clinical data", configurationState: "configuration_required", revision,
}),
workspaceSummaryFixture(alternateId, {
displayName: "Other Clinical", description: "Other data", configurationState: "configuration_required", revision: alternateRevision,
}),
]);
}),
http.get(`/api/workspaces/${alternateId}`, () => HttpResponse.json({ workspace: alternateWorkspace, revision: alternateRevision })),
http.get(`/api/workspaces/${alternateId}/runtime-configuration`, () => HttpResponse.json({
workspaceId: alternateId, revision: alternateRevision, configurationState: "configuration_required", requirements: [{ ...requirement }],
})),
http.post("/api/workspace-registry/pull", async () => {
started();
try {
await held;
return HttpResponse.json({ branch: "main", head: "b".repeat(40), ahead: 0, behind: 0, degraded: false });
} finally {
settled();
}
}),
);
const manager = renderManagerWithSettings();
await user.click(await screen.findByRole("button", { name: "Update workspace repository" }));
await requestStarted;
await user.click(screen.getByRole("button", { name: "Other Clinical" }));
expect(await screen.findByRole("heading", { name: "Other Clinical" })).toBeVisible();
await user.click(within(screen.getByRole("navigation", { name: "Workspaces" }))
.getByRole("button", { name: "Back to Level 1" }));
expect(await screen.findByRole("button", { name: "Update workspace repository" })).not.toBeDisabled();
const requestsBeforeRelease = { statusRequests, workspaceRequests };
act(() => release());
await act(async () => {
await requestSettled;
await new Promise((resolve) => { setTimeout(resolve, 50); });
});
expect({ statusRequests, workspaceRequests }).toEqual(requestsBeforeRelease);
expect(screen.queryByText("Workspace repository updated and validated.")).not.toBeInTheDocument();
});
test("a deferred repository update cannot update a context after workspace permission changes", async () => {
const user = userEvent.setup();
let release!: () => void;
let started!: () => void;
let settled!: () => void;
let statusRequests = 0;
let workspaceRequests = 0;
const held = new Promise<void>((resolve) => { release = resolve; });
const requestStarted = new Promise<void>((resolve) => { started = resolve; });
const requestSettled = new Promise<void>((resolve) => { settled = resolve; });
server.use(
http.get("/api/workspace-registry/status", () => {
statusRequests++;
return HttpResponse.json({ branch: "main", head: "a".repeat(40), ahead: 0, behind: 0, degraded: false });
}),
http.get("/api/workspaces", () => {
workspaceRequests++;
return HttpResponse.json([workspaceSummaryFixture("psd-clinical", {
displayName: "PSD Clinical", description: "Clinical data", configurationState: "configuration_required", revision,
})]);
}),
http.post("/api/workspace-registry/pull", async () => {
started();
try {
await held;
return HttpResponse.json({ branch: "main", head: "b".repeat(40), ahead: 0, behind: 0, degraded: false });
} finally {
settled();
}
}),
);
const manager = renderManagerWithSettings();
await user.click(await screen.findByRole("button", { name: "Update workspace repository" }));
await requestStarted;
manager.rerender({ canManageWorkspace: false });
manager.rerender({ canManageWorkspace: true });
expect(await screen.findByRole("button", { name: "Update workspace repository" })).not.toBeDisabled();
const requestsBeforeRelease = { statusRequests, workspaceRequests };
act(() => release());
await act(async () => {
await requestSettled;
await new Promise((resolve) => { setTimeout(resolve, 50); });
});
expect({ statusRequests, workspaceRequests }).toEqual(requestsBeforeRelease);
expect(screen.queryByText("Workspace repository updated and validated.")).not.toBeInTheDocument();
});
test("a deferred validation cannot update a context after workspace permission changes", async () => {
const user = userEvent.setup();
let release!: () => void;
let started!: () => void;
let settled!: () => void;
const held = new Promise<void>((resolve) => { release = resolve; });
const requestStarted = new Promise<void>((resolve) => { started = resolve; });
const requestSettled = new Promise<void>((resolve) => { settled = resolve; });
server.use(http.post("/api/workspaces/validate", async () => {
started();
try {
await held;
return HttpResponse.json({ workspace, contract: {}, activatable: true, diagnostics: [], authentication: readyAuthentication });
} finally {
settled();
}
}));
const manager = renderManagerWithSettings();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
await user.click(screen.getByRole("button", { name: "Validate workspace source" }));
await requestStarted;
manager.rerender({ canManageWorkspace: false });
manager.rerender({ canManageWorkspace: true });
expect(await screen.findByRole("button", { name: "Validate workspace source" })).not.toBeDisabled();
act(() => release());
await act(async () => {
await requestSettled;
await new Promise((resolve) => { setTimeout(resolve, 50); });
});
expect(screen.queryByTestId("workspace-authentication")).not.toBeInTheDocument();
});
test("a deferred connection test cannot update a context after workspace permission changes", async () => {
const user = userEvent.setup();
let release!: () => void;
let started!: () => void;
let settled!: () => void;
const held = new Promise<void>((resolve) => { release = resolve; });
const requestStarted = new Promise<void>((resolve) => { started = resolve; });
const requestSettled = new Promise<void>((resolve) => { settled = resolve; });
server.use(http.post("/api/workspaces/psd-clinical/test", async () => {
started();
try {
await held;
return HttpResponse.json({ activatable: true, diagnostics: [], authentication: readyAuthentication });
} finally {
settled();
}
}));
const manager = renderManagerWithSettings();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
await user.click(screen.getByRole("button", { name: "Test workspace connections" }));
await requestStarted;
manager.rerender({ canManageWorkspace: false });
manager.rerender({ canManageWorkspace: true });
expect(await screen.findByRole("button", { name: "Test workspace connections" })).not.toBeDisabled();
act(() => release());
await act(async () => {
await requestSettled;
await new Promise((resolve) => { setTimeout(resolve, 50); });
});
expect(screen.queryByTestId("workspace-authentication")).not.toBeInTheDocument();
});
test("closing clears unsaved secret fields", async () => {
const user = userEvent.setup();
const onClose = vi.fn();