This repository has been archived on 2026-08-17. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
beenvoice-app/tests/cross-client-parity.test.ts

105 lines
3.4 KiB
TypeScript

/// <reference types="bun" />
import { afterEach, describe, expect, test } from "bun:test";
import { fetchAuthCapabilities } from "../lib/auth-capabilities";
import { EXPENSE_CATEGORIES as appExpenseCategories } from "../lib/expense-categories";
import { getInvoiceStatus } from "../lib/invoice-status";
import { formatElapsedSeconds as formatAppElapsedSeconds } from "../lib/time-clock";
import { EXPENSE_CATEGORIES as webExpenseCategories } from "../../beenvoice-web/src/lib/expense-categories";
import { getEffectiveInvoiceStatus } from "../../beenvoice-web/src/lib/invoice-status";
import { safeCallbackPath } from "../../beenvoice-web/src/lib/safe-callback-url";
import {
formatElapsedSeconds as formatWebElapsedSeconds,
normalizeOptionalId,
} from "../../beenvoice-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 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(
formatWebElapsedSeconds(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([...webExpenseCategories]);
});
});