← 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');
});