← back to Costa Rica
test/admin-routes.test.js
96 lines
'use strict';
// Baseline coverage for routes/admin.js (previously ZERO tests). The admin API sits
// BEHIND the site basic-auth gate (mounted after it in server.js), so the router
// itself has no auth middleware — we mount it bare, as it runs in prod behind the gate.
// No real DB: pool.query is a queue mock.
//
// Endpoints: GET /stats, GET /claims, POST /claims/:placeId/:hostId, GET /bookings.
const { test, before, after } = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const express = require('express');
const db = require('../lib/db');
const adminRouter = require('../routes/admin');
let responses = [];
let calls = [];
const origQuery = db.pool.query;
let server, base;
before(async () => {
db.pool.query = async (sql, args) => { calls.push({ sql, args }); return responses.length ? responses.shift() : { rows: [], rowCount: 0 }; };
const app = express();
app.use(express.json());
app.use('/api/admin', adminRouter);
await new Promise(r => { server = app.listen(0, r); });
base = `http://127.0.0.1:${server.address().port}`;
});
after(() => { db.pool.query = origQuery; server && server.close(); });
function reset(resp) { responses = resp.slice(); calls = []; }
const lastArgs = () => calls.length ? calls[calls.length - 1].args : null;
function req(method, path, body) {
const data = body != null ? JSON.stringify(body) : null;
return new Promise((resolve, reject) => {
const r = http.request(base + path, { method, headers: {
...(data ? { 'content-type': 'application/json', 'content-length': Buffer.byteLength(data) } : {}) } },
res => { let b = ''; res.on('data', c => b += c); res.on('end', () => resolve({ status: res.statusCode, json: JSON.parse(b || '{}') })); });
r.on('error', reject); if (data) r.write(data); r.end();
});
}
test('GET /stats returns the marketplace KPI object', async () => {
reset([{ rows: [{ bookings: 12, confirmed: 5, gmv_minor: 400000, revenue_minor: 40000, pending_claims: 2, bookable: 8, hosts: 3 }] }]);
const r = await req('GET', '/api/admin/stats');
assert.equal(r.status, 200);
assert.equal(r.json.ok, true);
assert.equal(r.json.stats.bookings, 12);
assert.equal(r.json.stats.pending_claims, 2);
});
test('GET /claims defaults to pending and returns the claim rows', async () => {
reset([{ rows: [{ place_id: 5, host_id: 9, claim_status: 'pending', place_name: 'Casa', legal_name: 'H', email: 'h@x.c' }] }]);
const r = await req('GET', '/api/admin/claims');
assert.equal(r.status, 200);
assert.equal(r.json.claims.length, 1);
assert.equal(lastArgs()[0], 'pending', 'default status filter is pending');
});
test('GET /claims?status=all passes the "all" sentinel (no status filter)', async () => {
reset([{ rows: [] }]);
const r = await req('GET', '/api/admin/claims?status=all');
assert.equal(r.status, 200);
assert.equal(lastArgs()[0], 'all', 'status=all is forwarded so the WHERE short-circuits');
});
test('POST /claims/:placeId/:hostId rejects a bad decision with 400 BEFORE any DB write', async () => {
reset([]); // no query should run
const r = await req('POST', '/api/admin/claims/5/9', { decision: 'maybe' });
assert.equal(r.status, 400);
assert.equal(calls.length, 0, 'an invalid decision never reaches the UPDATE');
});
test('POST /claims/:placeId/:hostId approves a claim -> 200 + updated row', async () => {
reset([{ rows: [{ place_id: 5, host_id: 9, claim_status: 'approved' }] }]);
const r = await req('POST', '/api/admin/claims/5/9', { decision: 'approved' });
assert.equal(r.status, 200);
assert.equal(r.json.claim.claim_status, 'approved');
assert.deepEqual(lastArgs(), ['approved', '5', '9'], 'UPDATE parameterized with decision + route params (no injection)');
});
test('POST /claims/:placeId/:hostId on a nonexistent claim -> 404', async () => {
reset([{ rows: [] }]); // UPDATE matched nothing
const r = await req('POST', '/api/admin/claims/999/999', { decision: 'rejected' });
assert.equal(r.status, 404);
});
test('GET /bookings returns the oversight rows', async () => {
reset([{ rows: [{ code: 'CR-1', status: 'confirmed', total: 40000, place_name: 'Casa', traveler_email: 't@x.c' }] }]);
const r = await req('GET', '/api/admin/bookings');
assert.equal(r.status, 200);
assert.equal(r.json.bookings[0].code, 'CR-1');
});