Archived
105 lines
3.4 KiB
TypeScript
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]);
|
|
});
|
|
});
|