← back to Costa Rica
test/payout-methods-route.test.js
76 lines
'use strict';
// POST /host/payout-methods field-completeness validation (Cody cold audit, cycle 23).
// A payout method must carry the identifier its kind pays to, or the host silently
// gets $0 at payout time. These prove the route 400s an incomplete method BEFORE any
// INSERT, and accepts a complete one. No real DB: pool.query is a recording mock.
const { test, before, after } = require('node:test');
const assert = require('node:assert');
const http = require('node:http');
const express = require('express');
const { signToken } = require('../lib/auth');
const db = require('../lib/db');
const { router } = require('../routes/app');
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/app', router);
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(); });
const HOST = { rows: [{ id: 9, user_id: 3, legal_name: 'H', country: 'CR' }] }; // requireHost
function reset(resp) { responses = resp.slice(); calls = []; }
const hasSql = (re) => calls.some(c => re.test(c.sql));
function post(path, body, token) {
const data = JSON.stringify(body);
return new Promise((resolve, reject) => {
const r = http.request(base + path, { method: 'POST', headers: {
'content-type': 'application/json', 'content-length': Buffer.byteLength(data),
...(token ? { authorization: 'Bearer ' + token } : {}) } },
res => { let b = ''; res.on('data', c => b += c); res.on('end', () => resolve({ status: res.statusCode, json: JSON.parse(b || '{}') })); });
r.on('error', reject); r.end(data);
});
}
const token = () => signToken({ sub: 3, role: 'guest' });
test('POST /host/payout-methods: cr_iban WITHOUT an IBAN -> 400, no INSERT (would silently pay $0)', async () => {
reset([HOST]);
const r = await post('/api/app/host/payout-methods', { kind: 'cr_iban' }, token());
assert.equal(r.status, 400);
assert.match(r.json.error, /cr_iban requires/i);
assert.equal(hasSql(/INSERT INTO payout_methods/), false, 'an incomplete method must not be persisted');
});
test('POST /host/payout-methods: sinpe_movil WITHOUT a phone -> 400, no INSERT', async () => {
reset([HOST]);
const r = await post('/api/app/host/payout-methods', { kind: 'sinpe_movil' }, token());
assert.equal(r.status, 400);
assert.match(r.json.error, /sinpe_movil requires/i);
assert.equal(hasSql(/INSERT INTO payout_methods/), false);
});
test('POST /host/payout-methods: a COMPLETE sinpe_movil (with phone) is accepted -> 200 + INSERT', async () => {
reset([HOST, { rows: [{ id: 55, kind: 'sinpe_movil', currency: 'CRC', is_default: true }] }, { rowCount: 1 }]);
const r = await post('/api/app/host/payout-methods', { kind: 'sinpe_movil', sinpe_phone: '8888-0000' }, token());
assert.equal(r.status, 200);
assert.equal(r.json.payout_method.id, 55);
assert.ok(hasSql(/INSERT INTO payout_methods/), 'a complete method is persisted');
});
test('POST /host/payout-methods: an unknown kind -> 400', async () => {
reset([HOST]);
const r = await post('/api/app/host/payout-methods', { kind: 'venmo' }, token());
assert.equal(r.status, 400);
assert.match(r.json.error, /bad kind/i);
});