// @vitest-environment jsdom import { act } from "react"; import { createRoot } from "react-dom/client"; import { MemoryRouter } from "react-router-dom"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import type { CompanySecretProviderConfig, SecretProviderConfigDiscoveryPreviewResult, SecretProviderDescriptor, } from "@paperclipai/shared"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ProviderVaultsTab, Secrets } from "./Secrets"; import { ApiError } from "../api/client"; const mockSecretsApi = vi.hoisted(() => ({ list: vi.fn(), providers: vi.fn(), providerHealth: vi.fn(), providerConfigs: vi.fn(), providerConfigDiscoveryPreview: vi.fn(), createProviderConfig: vi.fn(), updateProviderConfig: vi.fn(), disableProviderConfig: vi.fn(), removeProviderConfig: vi.fn(), setDefaultProviderConfig: vi.fn(), checkProviderConfigHealth: vi.fn(), create: vi.fn(), update: vi.fn(), rotate: vi.fn(), disable: vi.fn(), enable: vi.fn(), archive: vi.fn(), remove: vi.fn(), usage: vi.fn(), accessEvents: vi.fn(), })); const mockSetBreadcrumbs = vi.hoisted(() => vi.fn()); const mockPushToast = vi.hoisted(() => vi.fn()); vi.mock("../api/secrets", () => ({ secretsApi: mockSecretsApi, })); vi.mock("../context/CompanyContext", () => ({ useCompany: () => ({ selectedCompanyId: "company-1", }), })); vi.mock("../context/BreadcrumbContext", () => ({ useBreadcrumbs: () => ({ setBreadcrumbs: mockSetBreadcrumbs, }), })); vi.mock("../context/ToastContext", () => ({ useToast: () => ({ pushToast: mockPushToast, }), useToastActions: () => ({ pushToast: mockPushToast, }), })); vi.mock("../context/SidebarContext", () => ({ useSidebar: () => ({ isMobile: false, }), })); // eslint-disable-next-line @typescript-eslint/no-explicit-any (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true; const providers: SecretProviderDescriptor[] = [ { id: "local_encrypted", label: "Local encrypted", requiresExternalRef: false, supportsManagedValues: true, supportsExternalReferences: false, configured: true, }, { id: "aws_secrets_manager", label: "AWS Secrets Manager", requiresExternalRef: false, supportsManagedValues: true, supportsExternalReferences: true, configured: true, }, { id: "gcp_secret_manager", label: "GCP Secret Manager", requiresExternalRef: false, supportsManagedValues: false, supportsExternalReferences: true, configured: false, }, { id: "vault", label: "Vault", requiresExternalRef: false, supportsManagedValues: false, supportsExternalReferences: true, configured: false, }, ]; const providerConfigs = [ { id: "vault-local", provider: "local_encrypted", displayName: "Local default", status: "ready", isDefault: true, healthStatus: "ready", healthCheckedAt: null, healthMessage: null, healthDetails: null, }, { id: "vault-aws", provider: "aws_secrets_manager", displayName: "AWS production", status: "ready", isDefault: false, healthStatus: null, healthCheckedAt: null, healthMessage: null, healthDetails: null, }, ] satisfies Partial[]; async function flushReact() { await act(async () => { await Promise.resolve(); await new Promise((resolve) => window.setTimeout(resolve, 0)); }); } function makeDiscoveryPreview( overrides: Partial = {}, ): SecretProviderConfigDiscoveryPreviewResult { return { provider: "aws_secrets_manager", nextToken: null, sampledSecretCount: 2, skippedForeignPaperclipSampleCount: 0, warnings: [], candidates: [ { provider: "aws_secrets_manager", displayName: "AWS production", config: { region: "us-east-1", namespace: "prod-use1", secretNamePrefix: "paperclip", kmsKeyId: "alias/paperclip-secrets", ownerTag: "platform", environmentTag: "production", }, sampleCount: 2, samples: [ { name: "paperclip/prod-use1/company-1/openai", hasKmsKey: true, tagKeys: ["owner", "environment"], }, ], signals: { namespace: "prod-use1", secretNamePrefix: "paperclip", environmentTag: "production", ownerTag: "platform", kmsKeyId: "alias/paperclip-secrets", hasKmsKey: true, sampleCount: 2, paperclipManagedSampleCount: 0, skippedForeignPaperclipSampleCount: 0, }, warnings: [], }, ], ...overrides, }; } function setInputValue(input: HTMLInputElement, value: string) { const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set; setter?.call(input, value); input.dispatchEvent(new Event("input", { bubbles: true })); } async function openAwsVaultDialog() { const vaultTabButton = [...document.querySelectorAll("button")].find( (button) => button.textContent?.includes("Provider vaults"), ) as HTMLButtonElement | undefined; await act(async () => { vaultTabButton?.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true })); vaultTabButton?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" })); vaultTabButton?.click(); }); await flushReact(); const addVaultButtons = [...document.querySelectorAll("button")].filter( (button) => button.textContent?.includes("Add vault"), ) as HTMLButtonElement[]; await act(async () => { addVaultButtons[1]?.click(); }); await flushReact(); } describe("Secrets page layout", () => { let container: HTMLDivElement; beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); mockSecretsApi.list.mockResolvedValue([]); mockSecretsApi.providers.mockResolvedValue(providers); mockSecretsApi.providerHealth.mockResolvedValue({ providers: [ { provider: "local_encrypted", status: "warn", message: "Local encrypted provider has a warning.", warnings: ["Backup reminder"], }, ], }); mockSecretsApi.providerConfigs.mockResolvedValue(providerConfigs); mockSecretsApi.providerConfigDiscoveryPreview.mockResolvedValue(makeDiscoveryPreview()); }); afterEach(() => { container.remove(); document.body.innerHTML = ""; vi.clearAllMocks(); }); it("uses the shared search/filter/tab affordances and keeps vault sections quiet", async () => { const root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); await act(async () => { root.render( , ); }); await flushReact(); await flushReact(); expect(container.querySelector('input[data-page-search-target="true"][aria-label="Search secrets"]')).not.toBeNull(); expect(container.textContent).toContain("Use secrets by binding them to runtime environment variables."); expect(container.textContent).toContain("GH_TOKEN"); expect(container.querySelectorAll("select")).toHaveLength(0); expect(container.textContent).not.toContain("Provider warnings detected"); expect(container.textContent).not.toContain("2/2 active"); await act(async () => { root.unmount(); }); const vaultRoot = createRoot(container); await act(async () => { vaultRoot.render( , ); }); await flushReact(); expect(container.querySelector('a[href="#provider-vaults-local_encrypted"]')).not.toBeNull(); expect(container.textContent).toContain("AWS production"); expect(container.textContent).not.toContain("Managed writes"); expect(container.textContent).not.toContain("External refs"); await act(async () => { vaultRoot.unmount(); }); }); it("warns that removing a provider vault only removes Paperclip config", async () => { mockSecretsApi.removeProviderConfig.mockResolvedValueOnce(providerConfigs[1]); const root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); await act(async () => { root.render( , ); }); await flushReact(); await flushReact(); const vaultTabButton = [...document.querySelectorAll("button")].find( (button) => button.textContent?.includes("Provider vaults"), ) as HTMLButtonElement | undefined; await act(async () => { vaultTabButton?.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true })); vaultTabButton?.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" })); vaultTabButton?.click(); }); await flushReact(); const removeButtons = [...document.querySelectorAll("button")].filter( (button) => button.textContent?.trim() === "Remove", ) as HTMLButtonElement[]; await act(async () => { removeButtons[1]?.click(); }); await flushReact(); expect(document.body.textContent).toContain("Remove provider vault"); expect(document.body.textContent).toContain("from Paperclip only"); expect(document.body.textContent).toContain("does not delete"); expect(document.body.textContent).toContain("AWS Secrets Manager"); const confirmButton = [...document.querySelectorAll("button")].find( (button) => button.textContent?.includes("Remove from Paperclip"), ) as HTMLButtonElement | undefined; await act(async () => { confirmButton?.click(); }); await flushReact(); expect(mockSecretsApi.removeProviderConfig).toHaveBeenCalledWith("vault-aws"); expect(mockSecretsApi.disableProviderConfig).not.toHaveBeenCalled(); await act(async () => { root.unmount(); }); }); it("opens reference details from the secrets table count", async () => { mockSecretsApi.list.mockResolvedValue([ { id: "secret-openai", companyId: "company-1", key: "openai_api_key", name: "OPENAI_API_KEY", provider: "local_encrypted", status: "active", managedMode: "paperclip_managed", externalRef: null, providerConfigId: null, providerMetadata: null, latestVersion: 1, description: null, lastResolvedAt: null, lastRotatedAt: null, deletedAt: null, createdByAgentId: null, createdByUserId: "user-1", referenceCount: 2, createdAt: new Date("2026-05-06T00:00:00.000Z"), updatedAt: new Date("2026-05-06T00:00:00.000Z"), }, ]); mockSecretsApi.usage.mockResolvedValue({ secretId: "secret-openai", bindings: [ { id: "binding-agent", companyId: "company-1", secretId: "secret-openai", targetType: "agent", targetId: "agent-1", configPath: "env.OPENAI_API_KEY", versionSelector: "latest", required: true, label: null, target: { type: "agent", id: "agent-1", label: "CodexCoder", href: "/agents/codexcoder", status: "idle", }, createdAt: new Date("2026-05-06T00:00:00.000Z"), updatedAt: new Date("2026-05-06T00:00:00.000Z"), }, ], }); const root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); await act(async () => { root.render( , ); }); await flushReact(); await flushReact(); const referencesButton = container.querySelector( 'button[aria-label="View references for OPENAI_API_KEY"]', ) as HTMLButtonElement | null; expect(referencesButton?.textContent).toBe("2"); await act(async () => { referencesButton?.click(); }); await flushReact(); expect(mockSecretsApi.usage).toHaveBeenCalledWith("secret-openai"); expect(document.body.textContent).toContain("Secret references"); expect(document.body.textContent).toContain("CodexCoder"); expect(document.body.textContent).toContain("env.OPENAI_API_KEY"); await act(async () => { root.unmount(); }); }); it("keeps the new secret value textarea width-constrained for long tokens", async () => { const root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); await act(async () => { root.render( , ); }); await flushReact(); await flushReact(); const newSecretButton = Array.from(container.querySelectorAll("button")).find( (button) => button.textContent?.includes("New secret"), ) as HTMLButtonElement | undefined; expect(newSecretButton).toBeDefined(); await act(async () => { newSecretButton?.click(); }); await flushReact(); const secretValueTextarea = document.body.querySelector("#new-secret-value") as HTMLTextAreaElement | null; expect(secretValueTextarea).not.toBeNull(); expect(secretValueTextarea?.className).toContain("min-w-0"); expect(secretValueTextarea?.className).toContain("overflow-x-hidden"); expect(secretValueTextarea?.className).toContain("break-all"); await act(async () => { root.unmount(); }); }); it("discovers AWS provider vault candidates and applies selected values as prefill", async () => { mockSecretsApi.providerConfigDiscoveryPreview.mockResolvedValueOnce(makeDiscoveryPreview()); const root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); await act(async () => { root.render( , ); }); await flushReact(); await flushReact(); await openAwsVaultDialog(); const discoveryButton = document.querySelector( '[data-testid="aws-vault-discovery-button"]', ) as HTMLButtonElement | null; expect(discoveryButton).not.toBeNull(); expect(discoveryButton?.disabled).toBe(true); const regionInput = document.getElementById("provider-vault-aws-region") as HTMLInputElement | null; const prefixInput = document.getElementById("provider-vault-secret-name-prefix") as HTMLInputElement | null; expect(regionInput).not.toBeNull(); await act(async () => { setInputValue(regionInput!, "us-east-1"); setInputValue(prefixInput!, "paperclip"); }); await flushReact(); expect(discoveryButton?.disabled).toBe(false); await act(async () => { discoveryButton?.click(); }); await flushReact(); await flushReact(); expect(mockSecretsApi.providerConfigDiscoveryPreview).toHaveBeenCalledWith("company-1", { provider: "aws_secrets_manager", config: { region: "us-east-1", namespace: null, secretNamePrefix: "paperclip", kmsKeyId: null, ownerTag: null, environmentTag: null, }, query: "paperclip", pageSize: 25, }); expect(document.body.textContent).toContain("AWS production"); const useValuesButton = [...document.querySelectorAll("button")].find( (button) => button.textContent?.includes("Use values"), ) as HTMLButtonElement | undefined; await act(async () => { useValuesButton?.click(); }); await flushReact(); expect((document.getElementById("vault-name") as HTMLInputElement).value).toBe("AWS production"); expect((document.getElementById("provider-vault-namespace") as HTMLInputElement).value).toBe("prod-use1"); expect((document.getElementById("provider-vault-secret-name-prefix") as HTMLInputElement).value).toBe("paperclip"); expect((document.getElementById("provider-vault-kms-key-id") as HTMLInputElement).value).toBe("alias/paperclip-secrets"); expect((document.getElementById("provider-vault-owner-tag") as HTMLInputElement).value).toBe("platform"); expect((document.getElementById("provider-vault-environment-tag") as HTMLInputElement).value).toBe("production"); expect(mockSecretsApi.createProviderConfig).not.toHaveBeenCalled(); await act(async () => { root.unmount(); }); }); it("shows AWS discovery errors without replacing manual vault form values", async () => { mockSecretsApi.providerConfigDiscoveryPreview.mockRejectedValueOnce( new ApiError("AWS Secrets Manager denied the request. Check IAM permissions for this provider vault.", 403, { details: { code: "access_denied" }, }), ); const root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); await act(async () => { root.render( , ); }); await flushReact(); await flushReact(); await openAwsVaultDialog(); const regionInput = document.getElementById("provider-vault-aws-region") as HTMLInputElement; const namespaceInput = document.getElementById("provider-vault-namespace") as HTMLInputElement; await act(async () => { setInputValue(regionInput, "us-west-2"); setInputValue(namespaceInput, "manual-prod"); }); await flushReact(); const discoveryButton = document.querySelector( '[data-testid="aws-vault-discovery-button"]', ) as HTMLButtonElement | null; await act(async () => { discoveryButton?.click(); }); await flushReact(); await flushReact(); expect(document.body.textContent).toContain("AWS Secrets Manager denied the request"); expect(regionInput.value).toBe("us-west-2"); expect(namespaceInput.value).toBe("manual-prod"); await act(async () => { root.unmount(); }); }); it("shows an empty AWS discovery result without blocking manual entry", async () => { mockSecretsApi.providerConfigDiscoveryPreview.mockResolvedValueOnce( makeDiscoveryPreview({ candidates: [], sampledSecretCount: 0 }), ); const root = createRoot(container); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } }, }); await act(async () => { root.render( , ); }); await flushReact(); await flushReact(); await openAwsVaultDialog(); const regionInput = document.getElementById("provider-vault-aws-region") as HTMLInputElement; await act(async () => { setInputValue(regionInput, "us-east-2"); }); await flushReact(); await act(async () => { (document.querySelector('[data-testid="aws-vault-discovery-button"]') as HTMLButtonElement | null)?.click(); }); await flushReact(); await flushReact(); expect(document.body.textContent).toContain("No AWS vault metadata candidates found"); expect(regionInput.value).toBe("us-east-2"); await act(async () => { root.unmount(); }); }); });