install token now shown in dashboard until install completes.
This commit is contained in:
18 files changed
+1977
-15
No files matched your search
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { generateBusinessEnv, type BusinessEnvValues } from "../compose-template";
|
||||
|
||||
const baseValues: BusinessEnvValues = {
|
||||
name: "Acme Barbers",
|
||||
domain: "acme.example.com",
|
||||
databaseName: "acme",
|
||||
databaseUser: "acme",
|
||||
databasePassword: "db-password",
|
||||
authSecret: "auth-secret",
|
||||
installationToken: "the-installation-token-value",
|
||||
cronSecret: "cron-secret",
|
||||
};
|
||||
|
||||
describe("generateBusinessEnv — INSTALLATION_TOKEN", () => {
|
||||
it("writes the given installation token into the generated .env", () => {
|
||||
const env = generateBusinessEnv(baseValues);
|
||||
expect(env).toContain('INSTALLATION_TOKEN="the-installation-token-value"');
|
||||
});
|
||||
|
||||
it("never writes the token in a NEXT_PUBLIC_* (build-time-inlined) variable", () => {
|
||||
const env = generateBusinessEnv(baseValues);
|
||||
const publicLines = env.split("\n").filter((line) => line.startsWith("NEXT_PUBLIC_"));
|
||||
for (const line of publicLines) {
|
||||
expect(line).not.toContain("the-installation-token-value");
|
||||
}
|
||||
});
|
||||
|
||||
it("is the only place a business's INSTALLATION_TOKEN gets written — every business gets a distinct token if given one", () => {
|
||||
const envA = generateBusinessEnv({ ...baseValues, installationToken: "token-a" });
|
||||
const envB = generateBusinessEnv({ ...baseValues, domain: "other.example.com", installationToken: "token-b" });
|
||||
expect(envA).toContain('INSTALLATION_TOKEN="token-a"');
|
||||
expect(envB).toContain('INSTALLATION_TOKEN="token-b"');
|
||||
expect(envA).not.toContain("token-b");
|
||||
expect(envB).not.toContain("token-a");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fetchInstallationStatus, getInstallationToken, resolveInstallationView } from "../installation-status";
|
||||
|
||||
describe("fetchInstallationStatus", () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it("reports installed with installedAt when the tenant's /api/health says installed: true", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ status: "healthy", database: "reachable", installed: true, installedAt: "2026-01-15T10:00:00.000Z" }),
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const result = await fetchInstallationStatus("acme");
|
||||
expect(result).toEqual({ status: "installed", installedAt: "2026-01-15T10:00:00.000Z" });
|
||||
});
|
||||
|
||||
it("reports not_installed (with no installedAt) when installed: false", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ status: "healthy", database: "reachable", installed: false, installedAt: null }),
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const result = await fetchInstallationStatus("acme");
|
||||
expect(result).toEqual({ status: "not_installed", installedAt: null });
|
||||
});
|
||||
|
||||
it("reports unknown — never not_installed — when the fetch throws (container down/unreachable)", async () => {
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
|
||||
|
||||
const result = await fetchInstallationStatus("acme");
|
||||
expect(result).toEqual({ status: "unknown", installedAt: null });
|
||||
});
|
||||
|
||||
it("reports unknown when the response isn't ok (e.g. 503 unhealthy)", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ ok: false, json: async () => ({}) }) as unknown as typeof fetch;
|
||||
|
||||
const result = await fetchInstallationStatus("acme");
|
||||
expect(result).toEqual({ status: "unknown", installedAt: null });
|
||||
});
|
||||
|
||||
it("reports unknown for a malformed body (no boolean `installed` field)", async () => {
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ unexpected: true }) }) as unknown as typeof fetch;
|
||||
|
||||
const result = await fetchInstallationStatus("acme");
|
||||
expect(result).toEqual({ status: "unknown", installedAt: null });
|
||||
});
|
||||
|
||||
it("targets the internal docker network hostname for the given slug, not the public domain", async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ installed: false }) });
|
||||
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
||||
|
||||
await fetchInstallationStatus("acme-barbers");
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith("http://next-chair-acme-barbers:3000/api/health", expect.any(Object));
|
||||
});
|
||||
});
|
||||
|
||||
describe("getInstallationToken / resolveInstallationView", () => {
|
||||
let stacksDir: string;
|
||||
const originalStacksDir = process.env.STACKS_DIR;
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
beforeEach(async () => {
|
||||
stacksDir = await mkdtemp(path.join(tmpdir(), "nc-manager-test-"));
|
||||
process.env.STACKS_DIR = stacksDir;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (originalStacksDir === undefined) delete process.env.STACKS_DIR;
|
||||
else process.env.STACKS_DIR = originalStacksDir;
|
||||
await rm(stacksDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function writeStackEnv(slug: string, envContent: string) {
|
||||
const dir = path.join(stacksDir, slug);
|
||||
await mkdir(dir, { recursive: true });
|
||||
await writeFile(path.join(dir, ".env"), envContent, "utf8");
|
||||
}
|
||||
|
||||
it("reads the existing token straight from that business's own .env — an already-generated token is preserved, not regenerated", async () => {
|
||||
await writeStackEnv("acme", 'BUSINESS_NAME="Acme"\nINSTALLATION_TOKEN="existing-token-value"\n');
|
||||
|
||||
const token = await getInstallationToken("acme");
|
||||
expect(token).toBe("existing-token-value");
|
||||
});
|
||||
|
||||
it("returns null (not an error) for a business with no token in its .env", async () => {
|
||||
await writeStackEnv("legacy-biz", 'BUSINESS_NAME="Legacy"\n');
|
||||
|
||||
const token = await getInstallationToken("legacy-biz");
|
||||
expect(token).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for a business directory that doesn't exist", async () => {
|
||||
const token = await getInstallationToken("does-not-exist");
|
||||
expect(token).toBeNull();
|
||||
});
|
||||
|
||||
it("tenant isolation: never returns another business's token", async () => {
|
||||
await writeStackEnv("tenant-a", 'INSTALLATION_TOKEN="token-for-a"\n');
|
||||
await writeStackEnv("tenant-b", 'INSTALLATION_TOKEN="token-for-b"\n');
|
||||
|
||||
expect(await getInstallationToken("tenant-a")).toBe("token-for-a");
|
||||
expect(await getInstallationToken("tenant-b")).toBe("token-for-b");
|
||||
});
|
||||
|
||||
it("resolveInstallationView never includes a token once installed — the token is not even read from disk in that branch", async () => {
|
||||
await writeStackEnv("acme", 'INSTALLATION_TOKEN="should-never-appear"\n');
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ installed: true, installedAt: "2026-01-15T10:00:00.000Z" }),
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const view = await resolveInstallationView("acme", "acme.example.com");
|
||||
|
||||
expect(view).toEqual({ kind: "installed", installedAt: "2026-01-15T10:00:00.000Z" });
|
||||
expect(JSON.stringify(view)).not.toContain("should-never-appear");
|
||||
});
|
||||
|
||||
it("resolveInstallationView surfaces the token pre-install, with the public install URL", async () => {
|
||||
await writeStackEnv("acme", 'INSTALLATION_TOKEN="real-token"\n');
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ installed: false }) }) as unknown as typeof fetch;
|
||||
|
||||
const view = await resolveInstallationView("acme", "acme.example.com");
|
||||
|
||||
expect(view).toEqual({ kind: "not_installed", token: "real-token", installUrl: "https://acme.example.com/install" });
|
||||
});
|
||||
|
||||
it("resolveInstallationView still surfaces the token (not hidden) when status can't be confirmed", async () => {
|
||||
await writeStackEnv("acme", 'INSTALLATION_TOKEN="real-token"\n');
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error("ECONNREFUSED"));
|
||||
|
||||
const view = await resolveInstallationView("acme", "acme.example.com");
|
||||
|
||||
expect(view.kind).toBe("unknown");
|
||||
expect(view).toMatchObject({ token: "real-token" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { randomHex } from "../random";
|
||||
|
||||
describe("randomHex", () => {
|
||||
it("returns a hex string of the requested byte length", () => {
|
||||
const value = randomHex(32);
|
||||
expect(value).toMatch(/^[a-f0-9]+$/);
|
||||
expect(value).toHaveLength(64);
|
||||
});
|
||||
|
||||
it("is sufficiently long/unpredictable — two calls never collide", () => {
|
||||
const a = randomHex(32);
|
||||
const b = randomHex(32);
|
||||
expect(a).not.toBe(b);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user