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