179 lines
5.3 KiB
JavaScript
179 lines
5.3 KiB
JavaScript
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);
|
|
});
|