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( , ), }; } function renderDeniedManager() { const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); return render( , ); } 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((resolve) => { release = resolve; }); const requestStarted = new Promise((resolve) => { started = resolve; }); const requestSettled = new Promise((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( , ); 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(); });