import assert from "node:assert/strict"; import { readFile } from "node:fs/promises"; import test from "node:test"; import vm from "node:vm"; const source = await readFile(new URL("../api-client.js", import.meta.url), "utf8"); function jsonResponse(payload, status = 200) { return { ok: status >= 200 && status < 300, status, async json() { return payload; } }; } function loadClient({ search = "", config = {}, fetchImpl = async () => jsonResponse({}) } = {}) { const windowObject = { location: { search, origin: "http://127.0.0.1:4173" }, CONDO_RUNTIME_CONFIG: { defaultMode: "demo", apiBaseUrl: "http://127.0.0.1:3000", periodYear: 2026, ...config }, fetch: fetchImpl, setTimeout, clearTimeout }; vm.runInNewContext(source, { window: windowObject, URL, URLSearchParams, AbortController, Error, Object, Math, Number, String, JSON }); return windowObject.CONDO_API; } test("runtime requires an explicit api query mode", () => { assert.equal(loadClient().runtime.mode, "demo"); assert.equal(loadClient({ search: "?mode=api" }).runtime.mode, "api"); assert.equal(loadClient({ search: "?mode=unexpected" }).runtime.mode, "demo"); }); test("owner pagination collects every API page", async () => { const requestedPages = []; const api = loadClient({ search: "?mode=api", fetchImpl: async url => { const page = Number(url.searchParams.get("page")); requestedPages.push(page); const itemCount = page === 1 ? 100 : 90; return jsonResponse({ items: Array.from({ length: itemCount }, (_, index) => ({ id: `${page}-${index}` })), total: 190, page, pageSize: 100, periodYear: 2026 }); } }); const result = await api.client.listAllOwnerAccounts(); assert.deepEqual(requestedPages, [1, 2]); assert.equal(result.total, 190); assert.equal(result.items.length, 190); }); test("API failures expose stable status and code", async () => { const api = loadClient({ search: "?mode=api", fetchImpl: async () => jsonResponse({ error: { code: "INSUFFICIENT_BALANCE", message: "Insufficient balance" } }, 422) }); await assert.rejects( api.client.getDashboard(), error => error.name === "CondoApiError" && error.status === 422 && error.code === "INSUFFICIENT_BALANCE" ); }); test("authentication uses credentialed requests and the exact login contract", async () => { const requests = []; const api = loadClient({ fetchImpl: async (url, options) => { requests.push({ url, options }); if (url.pathname === "/auth/session") { return jsonResponse({ authenticated: false }); } if (url.pathname === "/auth/login") { return jsonResponse({ authenticated: true, user: { username: "wyndhamcondon" }, expiresAt: "2026-08-02T12:00:00.000Z" }); } return jsonResponse({ authenticated: false }); } }); await api.client.getSession(); await api.client.login({ username: "wyndhamcondon", password: "wyndhamcondon" }); await api.client.logout(); assert.deepEqual(requests.map(request => request.url.pathname), [ "/auth/session", "/auth/login", "/auth/logout" ]); requests.forEach(request => assert.equal(request.options.credentials, "include")); assert.equal(requests[0].options.method, "GET"); assert.equal(requests[1].options.method, "POST"); assert.deepEqual(JSON.parse(requests[1].options.body), { username: "wyndhamcondon", password: "wyndhamcondon" }); assert.equal(requests[2].options.method, "POST"); assert.equal(requests[2].options.body, undefined); }); test("usage creation sends only the supplied API contract", async () => { let captured; const api = loadClient({ search: "?mode=api", fetchImpl: async (url, options) => { captured = { url, options }; return jsonResponse({ id: "record-id" }, 201); } }); const input = { ownerAccountId: "00000000-0000-4000-8000-000000000001", confirmationNo: "26090001", checkIn: "2026-09-01", checkOut: "2026-09-04", usedRoomType: "SU1", manualMultiplier: null, remark: "", idempotencyKey: "00000000-0000-4000-8000-000000000002" }; await api.client.createUsageRecord(input); assert.equal(captured.url.pathname, "/usage-records"); assert.equal(captured.options.method, "POST"); assert.equal(captured.options.credentials, "include"); assert.deepEqual(JSON.parse(captured.options.body), input); assert.equal("night" in JSON.parse(captured.options.body), false); assert.equal("use" in JSON.parse(captured.options.body), false); assert.equal("balance" in JSON.parse(captured.options.body), false); }); test("usage deletion uses the record route and DELETE method", async () => { let captured; const api = loadClient({ search: "?mode=api", fetchImpl: async (url, options) => { captured = { url, options }; return jsonResponse(null, 204); } }); await api.client.deleteUsageRecord("00000000-0000-4000-8000-000000000002"); assert.equal(captured.url.pathname, "/usage-records/00000000-0000-4000-8000-000000000002"); assert.equal(captured.options.method, "DELETE"); assert.equal(captured.options.credentials, "include"); assert.equal(captured.options.body, undefined); });