import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const composeUpMock = vi.fn().mockResolvedValue(undefined); vi.mock("@/lib/docker", () => ({ composeUp: (...args: unknown[]) => composeUpMock(...args), composeDown: vi.fn(), composeLogs: vi.fn(), composeRestart: vi.fn(), })); const fetchInstallationStatusMock = vi.fn(); vi.mock("@/lib/installation-status", () => ({ fetchInstallationStatus: (...args: unknown[]) => fetchInstallationStatusMock(...args), })); vi.mock("next/cache", () => ({ revalidatePath: vi.fn() })); import { regenerateInstallationTokenAction } from "../actions"; const TOKEN_HEX_PATTERN = /^[a-f0-9]{64}$/; describe("regenerateInstallationTokenAction", () => { let stacksDir: string; const originalStacksDir = process.env.STACKS_DIR; beforeEach(async () => { stacksDir = await mkdtemp(path.join(tmpdir(), "nc-manager-actions-test-")); process.env.STACKS_DIR = stacksDir; composeUpMock.mockClear(); fetchInstallationStatusMock.mockReset(); fetchInstallationStatusMock.mockResolvedValue({ status: "not_installed", installedAt: null }); }); afterEach(async () => { if (originalStacksDir === undefined) delete process.env.STACKS_DIR; else process.env.STACKS_DIR = originalStacksDir; await rm(stacksDir, { recursive: true, force: true }); }); async function createStack(slug: string, envContent: string) { const dir = path.join(stacksDir, slug); await mkdir(dir, { recursive: true }); await writeFile(path.join(dir, "compose.yaml"), "name: test\n", "utf8"); await writeFile(path.join(dir, ".env"), envContent, "utf8"); return dir; } function formData(fields: Record): FormData { const fd = new FormData(); for (const [key, value] of Object.entries(fields)) fd.set(key, value); return fd; } it("requires confirmation on the first call — does not touch the .env or the container yet", async () => { await createStack("acme", 'INSTALLATION_TOKEN="old-token"\n'); const result = await regenerateInstallationTokenAction("acme", { error: null, needsConfirmation: false, token: null }, formData({})); expect(result).toEqual({ error: null, needsConfirmation: true, token: null }); expect(composeUpMock).not.toHaveBeenCalled(); const envAfter = await readFile(path.join(stacksDir, "acme", ".env"), "utf8"); expect(envAfter).toContain("old-token"); }); it("on confirmation, replaces the token, recreates the container, and returns the new token", async () => { await createStack("acme", 'BUSINESS_NAME="Acme"\nINSTALLATION_TOKEN="old-token"\nAUTH_SECRET="unrelated-secret"\n'); const result = await regenerateInstallationTokenAction( "acme", { error: null, needsConfirmation: true, token: null }, formData({ confirmRegenerate: "true" }), ); expect(result.error).toBeNull(); expect(result.token).toMatch(TOKEN_HEX_PATTERN); expect(result.token).not.toBe("old-token"); const envAfter = await readFile(path.join(stacksDir, "acme", ".env"), "utf8"); expect(envAfter).toContain(`INSTALLATION_TOKEN="${result.token}"`); expect(envAfter).not.toContain("old-token"); // Rewriting the token must not touch unrelated secrets already in the file. expect(envAfter).toContain('AUTH_SECRET="unrelated-secret"'); expect(composeUpMock).toHaveBeenCalledWith(path.join(stacksDir, "acme"), { forceRecreate: true }); }); it("invalidates the previous token — the old value no longer appears anywhere in the business's config", async () => { await createStack("acme", 'INSTALLATION_TOKEN="old-token-to-invalidate"\n'); await regenerateInstallationTokenAction("acme", { error: null, needsConfirmation: true, token: null }, formData({ confirmRegenerate: "true" })); const envAfter = await readFile(path.join(stacksDir, "acme", ".env"), "utf8"); expect(envAfter).not.toContain("old-token-to-invalidate"); }); it("refuses to regenerate once installation is complete, even if the form is (re)submitted", async () => { await createStack("acme", 'INSTALLATION_TOKEN="old-token"\n'); fetchInstallationStatusMock.mockResolvedValue({ status: "installed", installedAt: "2026-01-01T00:00:00.000Z" }); const result = await regenerateInstallationTokenAction( "acme", { error: null, needsConfirmation: true, token: null }, formData({ confirmRegenerate: "true" }), ); expect(result.error).toMatch(/already completed installation/i); expect(result.token).toBeNull(); expect(composeUpMock).not.toHaveBeenCalled(); const envAfter = await readFile(path.join(stacksDir, "acme", ".env"), "utf8"); expect(envAfter).toContain("old-token"); }); it("tenant isolation: regenerating one business's token never touches a sibling business's .env", async () => { await createStack("tenant-a", 'INSTALLATION_TOKEN="token-a"\n'); await createStack("tenant-b", 'INSTALLATION_TOKEN="token-b"\n'); await regenerateInstallationTokenAction("tenant-a", { error: null, needsConfirmation: true, token: null }, formData({ confirmRegenerate: "true" })); const envB = await readFile(path.join(stacksDir, "tenant-b", ".env"), "utf8"); expect(envB).toContain('INSTALLATION_TOKEN="token-b"'); expect(composeUpMock).toHaveBeenCalledTimes(1); expect(composeUpMock).toHaveBeenCalledWith(path.join(stacksDir, "tenant-a"), { forceRecreate: true }); }); it("rejects an unknown/nonexistent slug", async () => { await expect( regenerateInstallationTokenAction("does-not-exist", { error: null, needsConfirmation: true, token: null }, formData({ confirmRegenerate: "true" })), ).rejects.toThrow(/no such stack/i); }); });