123 lines
3.9 KiB
TypeScript
123 lines
3.9 KiB
TypeScript
/// <reference types="bun" />
|
|
|
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
import {
|
|
EXPENSE_CATEGORIES as domainExpenseCategories,
|
|
addCalendarDays,
|
|
calendarDateFromInstant,
|
|
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 timeZone = "America/New_York";
|
|
const today = calendarDateFromInstant(new Date(), timeZone);
|
|
const yesterday = addCalendarDays(today, -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, timeZone),
|
|
).toBe(fixture.expected);
|
|
expect(
|
|
getInvoiceStatus({
|
|
status: fixture.stored,
|
|
dueDate: fixture.dueDate,
|
|
createdBy: { timeZone },
|
|
}),
|
|
).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]);
|
|
});
|
|
});
|