Files
wyndham-Condon/backend/scripts/api-write-smoke.mjs
2026-08-02 13:33:13 +08:00

257 lines
7.3 KiB
JavaScript

import { randomUUID } from "node:crypto";
import process from "node:process";
import { createInterface } from "node:readline";
import pg from "pg";
import { buildApp } from "../dist/src/app.js";
import { PostgresCondoRepository } from "../dist/src/postgres-repository.js";
const { Pool } = pg;
async function readStandardInput() {
const lines = createInterface({
input: process.stdin,
terminal: false
});
for await (const line of lines) {
lines.close();
return line.trim();
}
throw Object.assign(new Error("Missing connection input"), {
code: "MISSING_CONNECTION_INPUT"
});
}
function safeError(error) {
return {
ok: false,
errorCode: typeof error?.code === "string"
? error.code
: "API_WRITE_SMOKE_FAILED"
};
}
const checks = {};
let pool;
let app;
let ownerId;
let periodId;
let testRoomNo;
let confirmationNo = "997000001";
let baselineCounts;
try {
const input = JSON.parse(await readStandardInput());
pool = new Pool({
host: input.host,
port: input.port,
user: input.user,
password: input.password,
database: input.database,
ssl: input.ssl ? { rejectUnauthorized: false } : undefined,
max: 3,
application_name: "condon_api_write_smoke",
connectionTimeoutMillis: 10_000,
options: "-c statement_timeout=30000 -c lock_timeout=5000"
});
const initialResult = await pool.query(`
SELECT
(SELECT count(*)::integer FROM condon.owner_accounts) AS owners,
(SELECT count(*)::integer FROM condon.entitlement_periods) AS periods,
(SELECT count(*)::integer FROM condon.usage_records) AS usage,
(SELECT count(*)::integer FROM condon.entitlement_ledger) AS ledger,
(SELECT count(*)::integer FROM condon.bookings) AS bookings
`);
baselineCounts = initialResult.rows[0];
checks.initiallyAvailable = true;
ownerId = randomUUID();
periodId = randomUUID();
testRoomNo = `T${ownerId.replaceAll("-", "").slice(0, 12)}`;
const setupClient = await pool.connect();
try {
await setupClient.query("BEGIN");
await setupClient.query(
`
INSERT INTO condon.owner_accounts (
id,
account_no,
transfer_date,
owner_name,
room_no,
purchased_room_type_code,
unit_no,
member_no
)
VALUES ($1, NULL, NULL, 'API Write Test Owner', $2, 'RM1', 'TEST/3', 'TEST-3')
`,
[ownerId, testRoomNo]
);
await setupClient.query(
`
SELECT condon.open_entitlement_period(
$1::uuid,
$2::uuid,
2097::smallint,
$3::uuid,
NULL::uuid
)
`,
[periodId, ownerId, randomUUID()]
);
await setupClient.query("COMMIT");
} catch (error) {
await setupClient.query("ROLLBACK").catch(() => undefined);
throw error;
} finally {
setupClient.release();
}
const repository = new PostgresCondoRepository(pool);
app = await buildApp({ repository, logger: false });
const idempotencyKey = randomUUID();
const payload = {
ownerAccountId: ownerId,
confirmationNo,
checkIn: "2097-03-01",
checkOut: "2097-03-03",
usedRoomType: "RM1",
remark: "API write smoke",
idempotencyKey
};
const created = await app.inject({
method: "POST",
url: "/usage-records",
payload
});
const createdBody = created.json();
checks.apiCreate =
created.statusCode === 201
&& createdBody.ownerAccountId === ownerId
&& createdBody.night === 2
&& createdBody.use === 2
&& createdBody.balance === 13;
const retried = await app.inject({
method: "POST",
url: "/usage-records",
payload
});
const retriedBody = retried.json();
checks.apiIdempotency =
retried.statusCode === 201
&& retriedBody.id === createdBody.id
&& retriedBody.balance === 13;
const dashboard = await app.inject({
method: "GET",
url: "/dashboard?year=2097"
});
checks.apiDashboardReflectsWrite =
dashboard.statusCode === 200
&& dashboard.json().ownerRooms === Number(baselineCounts.owners) + 1
&& dashboard.json().remainingPrivileges === 13
&& dashboard.json().used === 2;
const usage = await app.inject({
method: "GET",
url: `/usage-records?ownerAccountId=${ownerId}`
});
checks.apiHistoryReflectsWrite =
usage.statusCode === 200
&& usage.json().total === 1
&& usage.json().items[0].confirmationNo === confirmationNo;
const ownerBeyondPage = await app.inject({
method: "GET",
url: `/owner-accounts?q=${encodeURIComponent(testRoomNo)}&year=2097&page=2&pageSize=1`
});
const usageBeyondPage = await app.inject({
method: "GET",
url: `/usage-records?ownerAccountId=${ownerId}&page=2&pageSize=1`
});
checks.paginationTotalsSurviveEmptyPage =
ownerBeyondPage.statusCode === 200
&& ownerBeyondPage.json().total === 1
&& ownerBeyondPage.json().items.length === 0
&& usageBeyondPage.statusCode === 200
&& usageBeyondPage.json().total === 1
&& usageBeyondPage.json().items.length === 0;
} catch (error) {
process.stdout.write(`${JSON.stringify(safeError(error), null, 2)}\n`);
process.exitCode = 1;
} finally {
if (pool && ownerId && periodId) {
const cleanup = await pool.connect().catch(() => undefined);
if (cleanup) {
try {
await cleanup.query("BEGIN");
await cleanup.query(
"DELETE FROM condon.entitlement_ledger WHERE entitlement_period_id = $1",
[periodId]
);
await cleanup.query(
"DELETE FROM condon.usage_records WHERE owner_account_id = $1",
[ownerId]
);
await cleanup.query(
"DELETE FROM condon.bookings WHERE confirmation_no = $1",
[confirmationNo]
);
await cleanup.query(
"DELETE FROM condon.entitlement_periods WHERE id = $1",
[periodId]
);
await cleanup.query(
"DELETE FROM condon.owner_accounts WHERE id = $1",
[ownerId]
);
await cleanup.query("COMMIT");
checks.cleanup = true;
} catch {
await cleanup.query("ROLLBACK").catch(() => undefined);
checks.cleanup = false;
process.exitCode = 1;
} finally {
cleanup.release();
}
}
}
if (pool) {
try {
const finalResult = await pool.query(`
SELECT
(SELECT count(*)::integer FROM condon.owner_accounts) AS owners,
(SELECT count(*)::integer FROM condon.entitlement_periods) AS periods,
(SELECT count(*)::integer FROM condon.usage_records) AS usage,
(SELECT count(*)::integer FROM condon.entitlement_ledger) AS ledger,
(SELECT count(*)::integer FROM condon.bookings) AS bookings
`);
checks.finalBusinessTablesPreserved = Object.entries(baselineCounts).every(
([key, value]) => Number(finalResult.rows[0][key]) === Number(value)
);
} catch {
checks.finalBusinessTablesEmpty = false;
process.exitCode = 1;
}
}
if (app) {
await app.close().catch(() => {
process.exitCode = 1;
});
} else if (pool) {
await pool.end().catch(() => {
process.exitCode = 1;
});
}
if (Object.keys(checks).length > 0) {
const ok = Object.values(checks).every(Boolean);
process.stdout.write(`${JSON.stringify({ ok, checks }, null, 2)}\n`);
if (!ok) process.exitCode = 1;
}
}