fix(auth): close cancellation and workspace races
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user