/// import { afterEach, describe, expect, test } from "bun:test"; import { EXPENSE_CATEGORIES as domainExpenseCategories, formatElapsedSeconds as formatDomainElapsedSeconds, getEffectiveInvoiceStatus, } from "@beenvoice/domain"; import { fetchAuthCapabilities } from "../lib/auth-capabilities"; import { EXPENSE_CATEGORIES as appExpenseCategories } from "../lib/expense-categories"; import { getInvoiceStatus } from "../lib/invoice-status"; import { generateInvoiceNumber as generateMobileInvoiceNumber } from "../lib/invoice-number"; import { formatElapsedSeconds as formatAppElapsedSeconds } from "../lib/time-clock"; import { generateInvoiceNumber as generateWebInvoiceNumber } from "../../web/src/lib/draft-invoice"; import { safeCallbackPath } from "../../web/src/lib/safe-callback-url"; import { normalizeOptionalId, } from "../../web/src/lib/time-clock"; const originalFetch = globalThis.fetch; afterEach(() => { globalThis.fetch = originalFetch; }); describe("auth contract smoke checks", () => { test("mobile reads the server capabilities payload", async () => { globalThis.fetch = (async (input) => { expect(String(input)).toBe("https://beenvoice.app/api/auth/capabilities"); return Response.json({ authentik: true, signupsDisabled: true }); }) as typeof fetch; await expect( fetchAuthCapabilities("https://beenvoice.app/"), ).resolves.toEqual({ authentik: true, signupsDisabled: true, }); }); test("web auth callbacks stay on the same origin", () => { expect(safeCallbackPath("/dashboard/invoices?status=sent")).toBe( "/dashboard/invoices?status=sent", ); expect(safeCallbackPath("https://attacker.example")).toBe("/dashboard"); expect(safeCallbackPath("//attacker.example")).toBe("/dashboard"); }); }); describe("invoice parity", () => { test("web and mobile use the device-local date in invoice numbers", () => { const lateLocalEvening = new Date(2026, 7, 16, 23, 30, 0, 123); expect(generateMobileInvoiceNumber(lateLocalEvening)).toStartWith("INV-20260816-"); expect(generateWebInvoiceNumber(lateLocalEvening)).toStartWith("INV-20260816-"); }); test("web and mobile agree on draft, paid, sent, and overdue states", () => { const today = new Date(); today.setHours(0, 0, 0, 0); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const fixtures = [ { stored: "draft" as const, dueDate: yesterday, expected: "draft" as const, }, { stored: "paid" as const, dueDate: yesterday, expected: "paid" as const, }, { stored: "sent" as const, dueDate: today, expected: "sent" as const }, { stored: "sent" as const, dueDate: yesterday, expected: "overdue" as const, }, ]; for (const fixture of fixtures) { expect(getEffectiveInvoiceStatus(fixture.stored, fixture.dueDate)).toBe( fixture.expected, ); expect( getInvoiceStatus({ status: fixture.stored, dueDate: fixture.dueDate }), ).toBe(fixture.expected); } }); }); describe("timer parity", () => { test("elapsed time formatting is identical", () => { for (const seconds of [0, 59, 60, 3_661, 86_399]) { expect(formatAppElapsedSeconds(seconds)).toBe( formatDomainElapsedSeconds(seconds), ); } }); test("server normalizes optional client identifiers", () => { expect(normalizeOptionalId(undefined)).toBeNull(); expect(normalizeOptionalId(" ")).toBeNull(); expect(normalizeOptionalId(" client-1 ")).toBe("client-1"); }); }); describe("expense parity", () => { test("web and mobile expose the same category vocabulary", () => { expect([...appExpenseCategories]).toEqual([...domainExpenseCategories]); }); });