Files
ThothII/frontend/src/shell/WorkspaceManager.test.tsx
T

380 lines
17 KiB
TypeScript

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { act, render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { http, HttpResponse } from "msw";
import { beforeEach, expect, test, vi } from "vitest";
import { server } from "../test/msw";
import { canonicalWorkspaceFixture, workspaceRevisionFixture, workspaceSummaryFixture } from "../test/workspace-fixtures";
import { WorkspaceManager } from "./WorkspaceManager";
import { setAuthState } from "../auth/authState";
import { queryClient } from "../app/queryClient";
const workspace = canonicalWorkspaceFixture("psd-clinical");
const revision = workspaceRevisionFixture("psd-clinical");
const authenticatedWorkspaceUser = {
issuer: "local", subject: "workspace-user", roles: ["user"] as const,
permissions: ["workspace.manage", "workspace.secrets.manage"], isAdmin: false,
csrfToken: "w".repeat(43), session: null,
};
const requirement = {
id: "dwh.password",
connector: "dwh",
label: "Data warehouse password",
description: "Password used by the selected data warehouse connection.",
input: "password",
required: true,
configured: false,
};
const readyAuthentication = {
ready: true,
mode: "none",
checks: [{ level: "info", code: "auth_ready", message: "Authentication is ready." }],
};
function runtimeConfiguration(configured = false) {
return {
workspaceId: "psd-clinical",
revision,
configurationState: configured ? "ready" : "configuration_required",
requirements: [{ ...requirement, configured }],
};
}
function renderManager(onClose = vi.fn()) {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return {
onClose,
...render(
<QueryClientProvider client={client}>
<WorkspaceManager open onClose={onClose} canManageWorkspace canManageSecrets />
</QueryClientProvider>,
),
};
}
function renderDeniedManager() {
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
return render(
<QueryClientProvider client={client}>
<WorkspaceManager open onClose={vi.fn()} canManageWorkspace={false} canManageSecrets={false} />
</QueryClientProvider>,
);
}
beforeEach(() => {
localStorage.clear();
setAuthState(authenticatedWorkspaceUser);
server.use(
http.get("/api/workspace-registry/status", () => HttpResponse.json({
branch: "main",
head: "a".repeat(40),
ahead: 0,
behind: 0,
degraded: false,
repository: {
host: "git.example.test",
repository: "analytics/thoth-workspaces",
transport: "ssh",
},
})),
http.get("/api/workspaces", () => HttpResponse.json([
workspaceSummaryFixture("psd-clinical", {
displayName: "PSD Clinical",
description: "Clinical data",
configurationState: "configuration_required",
revision,
}),
])),
http.get("/api/workspaces/psd-clinical", () => HttpResponse.json({ workspace, revision })),
http.get("/api/workspaces/psd-clinical/runtime-configuration", () => (
HttpResponse.json(runtimeConfiguration())
)),
);
});
test("defaults workspace mutations and secrets to denied", async () => {
renderDeniedManager();
expect(await screen.findByRole("heading", { name: "Workspace management" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Update workspace repository" })).not.toBeInTheDocument();
await userEvent.click(await screen.findByRole("button", { name: "PSD Clinical" }));
expect(await screen.findByRole("heading", { name: "Workspace-specific actions" })).toBeVisible();
expect(screen.queryByRole("button", { name: "Validate workspace source" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Test workspace connections" })).not.toBeInTheDocument();
expect(screen.queryByRole("heading", { name: "Runtime secrets" })).not.toBeInTheDocument();
});
test("uses a tall viewport area and keeps the workspace content scrollable", () => {
renderManager();
const dialog = screen.getByRole("dialog");
expect(dialog).toHaveClass(
"h-[86vh]",
"w-[94vw]",
"sm:w-[70vw]",
"max-w-[94vw]",
);
expect(screen.getByRole("main")).toHaveClass("overflow-y-auto");
});
test("offers a button above the workspace list that returns to Level 1", async () => {
const user = userEvent.setup();
const onClose = vi.fn();
renderManager(onClose);
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
expect(await screen.findByRole("heading", { name: "Workspace-specific actions" })).toBeVisible();
const navigation = screen.getByRole("navigation", { name: "Workspaces" });
const backButton = within(navigation).getByRole("button", { name: "Back to Level 1" });
expect(backButton).toHaveClass("border-border", "bg-card", "w-full");
expect(backButton.compareDocumentPosition(within(navigation).getByText("Available workspaces"))
& Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
await user.click(backButton);
expect(onClose).not.toHaveBeenCalled();
expect(screen.getByTestId("workspace-overview")).toBeVisible();
expect(screen.queryByRole("heading", { name: "Workspace-specific actions" })).not.toBeInTheDocument();
});
test("level one explains the read-only Git sequence and the repository update button", async () => {
const user = userEvent.setup();
server.use(http.post("/api/workspace-registry/pull", () => HttpResponse.json({
branch: "main", head: "b".repeat(40), ahead: 0, behind: 0, degraded: false,
})));
renderManager();
expect(await screen.findByRole("heading", { name: "Workspace management" })).toBeVisible();
const overview = screen.getByTestId("workspace-overview");
expect(within(overview).getByText(/Git server such as GitHub, GitLab, or Gitea/i)).toBeVisible();
expect(within(overview).getByText(/configured during ThothII installation/i)).toBeVisible();
const repositoryStep = within(overview).getAllByRole("listitem")[0];
expect(repositoryStep).toHaveTextContent(/create a workspace repository/i);
expect(repositoryStep).toHaveTextContent(/one directory for each workspace/i);
expect(repositoryStep).toHaveTextContent(/thoth-workspaces\.yaml/i);
expect(repositoryStep).toHaveTextContent(/database connection/i);
expect(repositoryStep).toHaveTextContent(/Evidence sources/i);
expect(repositoryStep).toHaveTextContent(/vector-database collection/i);
expect(within(overview).getByRole("link", { name: /workspace authoring instructions on GitHub/i })).toHaveAttribute(
"href",
"https://github.com/mptyl/ThothII/blob/main/docs/install/local-workspace-registry.md#prepare-and-publish-a-workspace-source",
);
expect(within(overview).getAllByText(/managed read-only checkout/i)).toHaveLength(2);
expect(within(overview).getByText(/current active revision remains unchanged/i)).toBeVisible();
expect(within(overview).getByText(/No workspace selection is required/i)).toBeVisible();
expect(await within(overview).findByText("git.example.test/analytics/thoth-workspaces")).toBeVisible();
await user.click(screen.getByRole("button", { name: "Update workspace repository" }));
expect(await screen.findByText("Workspace repository updated and validated.")).toBeVisible();
expect(screen.getByText(/create a workspace repository/i)).toBeInTheDocument();
expect(screen.queryByText(/import|export|bundle/i)).not.toBeInTheDocument();
});
test("workspace-specific commands remain isolated until a workspace is selected", async () => {
const user = userEvent.setup();
renderManager();
expect(screen.queryByRole("heading", { name: "Workspace-specific actions" })).not.toBeInTheDocument();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
expect(await screen.findByRole("heading", { name: "Workspace-specific actions" })).toBeVisible();
expect(screen.getByText(/reads this revision without modifying or publishing it/i)).toBeVisible();
expect(screen.getByText(/checks workspace.yaml and the required workspace directories/i)).toBeVisible();
expect(screen.getByText(/temporary decrypted credentials/i)).toBeVisible();
const databaseField = screen.getByText("Database").parentElement;
expect(databaseField).not.toBeNull();
expect(databaseField).toHaveTextContent("engine: postgres");
expect(databaseField).toHaveTextContent("database: database");
expect(databaseField).toHaveTextContent("schema: public");
expect(screen.getByRole("button", { name: "Validate workspace source" })).toBeVisible();
expect(screen.getByRole("button", { name: "Test workspace connections" })).toBeVisible();
});
test("keeps validation and connection results inside their respective action cards", async () => {
const user = userEvent.setup();
server.use(
http.post("/api/workspaces/validate", () => HttpResponse.json({
workspace, contract: {}, activatable: true, diagnostics: [], authentication: readyAuthentication,
})),
http.post("/api/workspaces/psd-clinical/test", () => HttpResponse.json({
activatable: false,
diagnostics: [{ level: "error", code: "connector_unavailable", message: "Connector diagnostic failed." }],
authentication: readyAuthentication,
})),
);
renderManager();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
const validationCard = screen.getByTestId("workspace-validation-card");
const connectionCard = screen.getByTestId("workspace-connection-card");
await user.click(within(validationCard).getByRole("button", { name: "Validate workspace source" }));
const validationStatus = await within(validationCard).findByRole("status");
expect(validationStatus).toHaveTextContent("Workspace source and authentication are valid.");
expect(validationStatus).toHaveClass("text-emerald-700");
expect(within(connectionCard).queryByText("Workspace source and authentication are valid.")).not.toBeInTheDocument();
await user.click(within(connectionCard).getByRole("button", { name: "Test workspace connections" }));
expect(await within(connectionCard).findByRole("alert")).toHaveTextContent(
"connector_unavailable: Connector diagnostic failed.",
);
expect(within(validationCard).queryByText("connector_unavailable: Connector diagnostic failed.")).not.toBeInTheDocument();
});
test("renders one authentication section with configured-group errors and no unmapped-group list", async () => {
const user = userEvent.setup();
server.use(http.post("/api/workspaces/validate", () => HttpResponse.json({
workspace,
contract: {},
activatable: false,
diagnostics: [],
authentication: {
ready: false,
mode: "oidc",
checks: [{
level: "error",
code: "oidc_mapped_group_missing",
field: "Thoth Administrators",
message: "A configured authorization group does not exist.",
}],
},
})));
renderManager();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
await user.click(screen.getByRole("button", { name: "Validate workspace source" }));
const section = await screen.findByTestId("workspace-authentication");
expect(within(section).getByRole("heading", { name: "Authentication" })).toBeVisible();
expect(within(section).getByText("Failed")).toBeVisible();
expect(within(section).getByText("oidc_mapped_group_missing: Thoth Administrators — A configured authorization group does not exist.")).toBeVisible();
expect(within(section).queryByText(/unmapped/i)).not.toBeInTheDocument();
});
test("never renders a hostile authentication field rejected by the API decoder", async () => {
const user = userEvent.setup();
const attacker = "attacker-field-SENTINEL";
server.use(http.post("/api/workspaces/validate", () => HttpResponse.json({
workspace,
contract: {},
activatable: false,
diagnostics: [],
authentication: {
ready: false,
mode: "oidc",
checks: [{ level: "error", code: "oidc_secret_missing", message: "failure", field: attacker }],
},
})));
renderManager();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
await user.click(screen.getByRole("button", { name: "Validate workspace source" }));
expect(await screen.findByRole("alert")).toBeVisible();
expect(screen.queryByText(new RegExp(attacker))).not.toBeInTheDocument();
});
test("renders binding_ok as a green connection success", async () => {
const user = userEvent.setup();
server.use(
http.post("/api/workspaces/psd-clinical/test", () => HttpResponse.json({
activatable: true,
diagnostics: [{ level: "info", code: "binding_ok", message: "Installation bindings and diagnostics succeeded." }],
authentication: readyAuthentication,
})),
);
renderManager();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
const connectionCard = screen.getByTestId("workspace-connection-card");
await user.click(within(connectionCard).getByRole("button", { name: "Test workspace connections" }));
const connectionStatus = await within(connectionCard).findByRole("status");
expect(connectionStatus).toHaveTextContent("binding_ok: Installation bindings and diagnostics succeeded.");
expect(connectionStatus).toHaveClass("text-emerald-700");
expect(within(connectionCard).queryByRole("alert")).not.toBeInTheDocument();
});
test("secret fields are write-only, clear after blind save, and may be forgotten", async () => {
const user = userEvent.setup();
let savedBody: unknown;
server.use(
http.put("/api/workspaces/psd-clinical/secrets", async ({ request }) => {
savedBody = await request.json();
return HttpResponse.json(runtimeConfiguration(true));
}),
http.delete("/api/workspaces/psd-clinical/secrets/dwh.password", () => (
HttpResponse.json(runtimeConfiguration(false))
)),
);
renderManager();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
const input = await screen.findByLabelText("Data warehouse password");
expect(input).toHaveValue("");
expect(input).toHaveAttribute("type", "password");
expect(screen.getByText("Not configured")).toBeVisible();
await user.type(input, "one-time-password");
await user.click(screen.getByRole("button", { name: "Save entered secrets" }));
await waitFor(() => expect(savedBody).toEqual({
values: { "dwh.password": "one-time-password" },
}));
expect(input).toHaveValue("");
expect(await screen.findByText("Configured")).toBeVisible();
expect(screen.queryByDisplayValue("one-time-password")).not.toBeInTheDocument();
expect(localStorage.length).toBe(0);
await user.click(screen.getByRole("button", { name: "Forget stored Data warehouse password" }));
expect(await screen.findByText("Not configured")).toBeVisible();
});
test("a delayed runtime-secret save from user A cannot repopulate user B's cache or notice", async () => {
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();
}
}));
queryClient.clear();
setAuthState({ issuer: "local", subject: "user-a", roles: ["user"], permissions: ["workspace.secrets.manage"], isAdmin: false, csrfToken: "a".repeat(43), session: null });
render(
<QueryClientProvider client={queryClient}>
<WorkspaceManager open onClose={vi.fn()} canManageWorkspace canManageSecrets />
</QueryClientProvider>,
);
await userEvent.click(await screen.findByRole("button", { name: "PSD Clinical" }));
const input = await screen.findByLabelText("Data warehouse password");
await userEvent.type(input, "a-secret");
await userEvent.click(screen.getByRole("button", { name: "Save entered secrets" }));
await requestStarted;
act(() => setAuthState({ issuer: "local", subject: "user-b", roles: ["user"], permissions: ["workspace.secrets.manage"], isAdmin: false, csrfToken: "b".repeat(43), session: null }));
expect(queryClient.getQueryData(["workspace-runtime-configuration", "psd-clinical"])).toBeUndefined();
release();
await act(async () => { await requestSettled; });
expect(queryClient.getQueryData(["workspace-runtime-configuration", "psd-clinical"])).toBeUndefined();
expect(screen.queryByText("Runtime secrets saved. Stored values remain hidden.")).not.toBeInTheDocument();
});
test("closing clears unsaved secret fields", async () => {
const user = userEvent.setup();
const onClose = vi.fn();
renderManager(onClose);
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
await user.type(await screen.findByLabelText("Data warehouse password"), "unsaved-value");
await user.click(screen.getByRole("button", { name: "Close workspace management" }));
expect(onClose).toHaveBeenCalledTimes(1);
expect(screen.queryByDisplayValue("unsaved-value")).not.toBeInTheDocument();
});