import assert from "node:assert/strict"; import { readFileSync } from "node:fs"; import { test } from "node:test"; import { isAuthorized } from "../lib/auth"; import { readLimited } from "../lib/body"; import { incomingWins } from "../lib/conflict"; import { isRowId, isUuid } from "../lib/ids"; import { money } from "../lib/money"; import { normalizePhone } from "../lib/phone"; import { allowRequest, clientIp, resetRateLimit } from "../lib/rate-limit"; import { SYNC_TABLES, SYNC_TABLE_NAMES } from "../lib/schema"; import { EPOCH_ISO, prepareRow, resolveCursors } from "../lib/sync"; import { isoToSql, sqlDateToIso } from "../lib/time"; import { pickWorkshopJob, stageLabel } from "../lib/workshop"; const SPEC_TABLES = [ "staff_members", "shop_settings", "work_days", "attendance_records", "short_leaves", "overtime_logs", "customers", "vehicles", "job_cards", "odo_readings", "job_card_events", "part_types", "bike_models", "bike_groups", "bike_group_members", "catalog_parts", "catalog_part_models", "part_variants", "job_line_items", "invoices", "invoice_lines", "payments", "demand_logs", "sms_outbox", "publish_outbox", ]; test("normalizePhone matches the tablet", () => { assert.equal(normalizePhone("771234567"), "771234567"); assert.equal(normalizePhone("0771234567"), "0771234567"); assert.equal(normalizePhone("077 123 4567"), "0771234567"); assert.equal(normalizePhone("00771234567"), "0771234567"); assert.equal(normalizePhone("917712345678"), "7712345678"); assert.equal(normalizePhone(""), ""); }); test("money uses Rs and two fraction digits", () => { assert.equal(money(500), "Rs 500.00"); assert.equal(money(0), "Rs 0.00"); assert.match(money(1500), /^Rs 1,500\.00$/); }); test("uuid and row ids", () => { assert.equal(isUuid("3f1c2a90-7b14-4d2e-9c11-6a0b5e8d4f21"), true); assert.equal(isUuid("default"), false); assert.equal(isUuid("not-a-uuid"), false); assert.equal(isRowId("default"), true); assert.equal(isRowId("3f1c2a90-7b14-4d2e-9c11-6a0b5e8d4f21"), true); assert.equal(isRowId(""), false); assert.equal(isRowId("has space"), false); }); test("conflict rule is last write wins, equal incoming wins", () => { const older = new Date("2026-09-28T06:30:00.000Z"); const newer = new Date("2026-09-28T07:30:00.000Z"); assert.equal(incomingWins(null, older), true); assert.equal(incomingWins(older, newer), true); assert.equal(incomingWins(older, new Date(older.getTime())), true); assert.equal(incomingWins(newer, older), false); }); test("timestamps round-trip as UTC", () => { assert.equal(isoToSql("2026-09-28T06:30:00.000Z"), "2026-09-28 06:30:00.000"); assert.equal(sqlDateToIso("2026-09-28 06:30:00.000"), "2026-09-28T06:30:00.000Z"); assert.equal(isoToSql("not-a-date"), null); }); test("auth requires the bearer token", () => { const previous = process.env.SYNC_TOKEN; process.env.SYNC_TOKEN = "test-secret-value"; try { assert.equal(isAuthorized("Bearer test-secret-value"), true); assert.equal(isAuthorized("Bearer wrong-secret-value"), false); assert.equal(isAuthorized("Bearer test-secret-value extra"), false); assert.equal(isAuthorized("Basic test-secret-value"), false); assert.equal(isAuthorized(null), false); } finally { if (previous === undefined) delete process.env.SYNC_TOKEN; else process.env.SYNC_TOKEN = previous; } }); test("status rate limit is 10 per minute per address", () => { resetRateLimit(); const now = 1_000_000; for (let i = 0; i < 10; i += 1) assert.equal(allowRequest("203.0.113.5", now), true); assert.equal(allowRequest("203.0.113.5", now), false); assert.equal(allowRequest("203.0.113.9", now), true); assert.equal(allowRequest("203.0.113.5", now + 60_000), true); }); test("client ip is the last forwarded hop", () => { assert.equal(clientIp("1.2.3.4, 203.0.113.5"), "203.0.113.5"); assert.equal(clientIp(null), "direct"); }); test("readLimited caps the body", async () => { const small = new Request("http://127.0.0.1/", { method: "POST", body: "abc" }); assert.equal(await readLimited(small, 10), "abc"); const big = new Request("http://127.0.0.1/", { method: "POST", body: "x".repeat(20) }); assert.equal(await readLimited(big, 10), null); }); test("missing sync cursor is the epoch", () => { const resolved = resolveCursors(undefined); assert.equal(resolved.error, undefined); assert.equal(resolved.cursors.customers.updatedAt.toISOString(), EPOCH_ISO); assert.equal(resolved.cursors.customers.id, ""); assert.equal(Object.keys(resolved.cursors).length, SPEC_TABLES.length); const partial = resolveCursors({ customers: { updatedAt: "2026-09-28T06:30:00.000Z", id: "abc" }, }); assert.equal(partial.cursors.customers.id, "abc"); assert.equal(partial.cursors.invoices.updatedAt.toISOString(), EPOCH_ISO); assert.equal(resolveCursors({ customers: { updatedAt: "nope", id: "" } }).error, "invalid cursor"); }); test("prepareRow stores is_dirty itself and accepts the settings id", () => { const settings = prepareRow(SYNC_TABLES.shop_settings, { id: "default", name: "100kmph", sms_bill_template: "Bill {url}", sms_status_template: "Status", public_base_url: "https://example.test", updated_at: "2026-09-28T06:30:00.000Z", is_dirty: true, }); assert.equal(settings.ok, true); if (settings.ok) { assert.equal(settings.row.columns.includes("is_dirty"), false); assert.equal(settings.row.id, "default"); } const customer = prepareRow(SYNC_TABLES.customers, { id: "3f1c2a90-7b14-4d2e-9c11-6a0b5e8d4f21", phone: "771234567", updated_at: "2026-09-28T06:30:00.000Z", pin_hash: "should-not-stick", }); assert.equal(customer.ok, false); }); test("workshop job prefers the newest open stage", () => { const picked = pickWorkshopJob([ { id: "a", stage: "billed", createdAt: "2026-09-28T08:00:00.000Z" }, { id: "b", stage: "in_repair", createdAt: "2026-09-28T07:00:00.000Z" }, { id: "c", stage: "received", createdAt: "2026-09-28T06:00:00.000Z" }, ]); assert.equal(picked?.id, "b"); assert.equal( pickWorkshopJob([{ id: "a", stage: "billed", createdAt: "2026-09-28T08:00:00.000Z" }]), null, ); assert.equal(stageLabel("quality_check"), "Quality Check"); assert.equal(stageLabel("in_repair"), "In Repair"); }); test("schema matches the migration", () => { const sql = readFileSync("db/001_init.sql", "utf8"); assert.deepEqual(SYNC_TABLE_NAMES, SPEC_TABLES); assert.equal(sql.includes("FOREIGN KEY"), false); assert.match(sql, /Two tablets incrementing this will clash/); assert.match(sql, /CREATE TABLE IF NOT EXISTS bill_snapshots/); for (const name of SPEC_TABLES) { const match = sql.match(new RegExp(`CREATE TABLE IF NOT EXISTS ${name} \\(([\\s\\S]*?)\\n\\)`)); assert.ok(match, name); const body = match[1]; assert.match(body, /updated_at, id/); for (const column of SYNC_TABLES[name].columns) { assert.match(body, new RegExp(`\\b${column.name}\\b`)); } const hasPin = SYNC_TABLES[name].columns.some((column) => column.name === "pin_hash"); assert.equal(hasPin, name === "staff_members"); } const customers = sql.match(/CREATE TABLE IF NOT EXISTS customers \(([\s\S]*?)\n\)/); assert.match(customers?.[1] ?? "", /idx_customers_phone \(phone\)/); const invoices = sql.match(/CREATE TABLE IF NOT EXISTS invoices \(([\s\S]*?)\n\)/); assert.match(invoices?.[1] ?? "", /uq_invoices_public_token \(public_token\)/); const jobs = sql.match(/CREATE TABLE IF NOT EXISTS job_cards \(([\s\S]*?)\n\)/); assert.match(jobs?.[1] ?? "", /idx_job_cards_customer \(customer_id\)/); assert.match(jobs?.[1] ?? "", /idx_job_cards_vehicle \(vehicle_id\)/); }); test("public lookup code does not select private columns", () => { const status = readFileSync("lib/status.ts", "utf8"); for (const word of ["engineer_notes", "complaint", "pin_hash", "sale_price", "unit_price", "qty_on_hand"]) { assert.equal(status.includes(word), false, word); } const sync = readFileSync("lib/sync.ts", "utf8"); assert.equal(sync.includes("console."), false); });