import { mkdir, mkdtemp, 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(), composeStatus: vi.fn().mockResolvedValue("stopped"), reloadCaddy: vi.fn(), runMigrations: vi.fn(), })); describe("applyGoogleCalendarSettingsToAllBusinesses", () => { let stacksDir: string; const originalStacksDir = process.env.STACKS_DIR; beforeEach(async () => { stacksDir = await mkdtemp(path.join(tmpdir(), "nc-manager-gcal-rollout-test-")); process.env.STACKS_DIR = stacksDir; composeUpMock.mockClear(); }); 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; } it("writes the shared client ID/secret into every business and force-recreates each container", async () => { const { applyGoogleCalendarSettingsToAllBusinesses } = await import("../google-calendar-rollout"); await createStack("acme", ['BUSINESS_NAME="Acme"', 'DOMAIN="acme.example.com"', 'CALENDAR_TOKEN_ENCRYPTION_KEY="existing-key"'].join("\n") + "\n"); const results = await applyGoogleCalendarSettingsToAllBusinesses({ clientId: "shared-client-id", clientSecret: "shared-client-secret", }); expect(results).toEqual([{ slug: "acme", ok: true }]); const env = await readFile(path.join(stacksDir, "acme", ".env"), "utf8"); expect(env).toContain('GOOGLE_CALENDAR_CLIENT_ID="shared-client-id"'); expect(env).toContain('GOOGLE_CALENDAR_CLIENT_SECRET="shared-client-secret"'); expect(composeUpMock).toHaveBeenCalledWith(path.join(stacksDir, "acme"), { forceRecreate: true }); }); it("never touches GOOGLE_CALENDAR_REDIRECT_URI — it stays derived from each business's own domain", async () => { const { applyGoogleCalendarSettingsToAllBusinesses } = await import("../google-calendar-rollout"); await createStack( "acme", ['BUSINESS_NAME="Acme"', 'DOMAIN="acme.example.com"', 'GOOGLE_CALENDAR_REDIRECT_URI="https://acme.example.com/api/staff/calendar/google/callback"'].join( "\n", ) + "\n", ); await applyGoogleCalendarSettingsToAllBusinesses({ clientId: "shared-client-id", clientSecret: "shared-client-secret" }); const env = await readFile(path.join(stacksDir, "acme", ".env"), "utf8"); expect(env).toContain('GOOGLE_CALENDAR_REDIRECT_URI="https://acme.example.com/api/staff/calendar/google/callback"'); }); it("leaves an existing CALENDAR_TOKEN_ENCRYPTION_KEY untouched — it must never be shared across businesses", async () => { const { applyGoogleCalendarSettingsToAllBusinesses } = await import("../google-calendar-rollout"); await createStack("acme", ['BUSINESS_NAME="Acme"', 'DOMAIN="acme.example.com"', 'CALENDAR_TOKEN_ENCRYPTION_KEY="already-set-key"'].join("\n") + "\n"); await applyGoogleCalendarSettingsToAllBusinesses({ clientId: "shared-client-id", clientSecret: "shared-client-secret" }); const env = await readFile(path.join(stacksDir, "acme", ".env"), "utf8"); expect(env).toContain('CALENDAR_TOKEN_ENCRYPTION_KEY="already-set-key"'); }); it("backfills a fresh, distinct CALENDAR_TOKEN_ENCRYPTION_KEY for a business that never had one (pre-fix)", async () => { const { applyGoogleCalendarSettingsToAllBusinesses } = await import("../google-calendar-rollout"); await createStack("acme", ['BUSINESS_NAME="Acme"', 'DOMAIN="acme.example.com"', 'CALENDAR_TOKEN_ENCRYPTION_KEY=""'].join("\n") + "\n"); await createStack("other", ['BUSINESS_NAME="Other"', 'DOMAIN="other.example.com"', 'CALENDAR_TOKEN_ENCRYPTION_KEY=""'].join("\n") + "\n"); await applyGoogleCalendarSettingsToAllBusinesses({ clientId: "shared-client-id", clientSecret: "shared-client-secret" }); const envA = await readFile(path.join(stacksDir, "acme", ".env"), "utf8"); const envB = await readFile(path.join(stacksDir, "other", ".env"), "utf8"); const keyA = /CALENDAR_TOKEN_ENCRYPTION_KEY="([^"]*)"/.exec(envA)?.[1]; const keyB = /CALENDAR_TOKEN_ENCRYPTION_KEY="([^"]*)"/.exec(envB)?.[1]; expect(keyA).toBeTruthy(); expect(keyB).toBeTruthy(); expect(keyA).not.toBe(keyB); }); it("reports per-business failures without aborting the rest", async () => { const { applyGoogleCalendarSettingsToAllBusinesses } = await import("../google-calendar-rollout"); await createStack("acme", ['BUSINESS_NAME="Acme"', 'DOMAIN="acme.example.com"'].join("\n") + "\n"); await createStack("broken", ['BUSINESS_NAME="Broken"', 'DOMAIN="broken.example.com"'].join("\n") + "\n"); composeUpMock.mockImplementation(async (dir: string) => { if (dir.includes("broken")) throw new Error("container recreate failed"); }); const results = await applyGoogleCalendarSettingsToAllBusinesses({ clientId: "shared-client-id", clientSecret: "shared-client-secret" }); expect(results).toContainEqual({ slug: "acme", ok: true }); expect(results).toContainEqual({ slug: "broken", ok: false, error: "container recreate failed" }); }); });