Files
dashcaddy/dashcaddy-api/__tests__/routes/deploys.catalog.r4.test.js
T
Hermes Agent 5cf5dac281 [pending-judge] fix(r4): audit contract, subdomain forwarding, timeout kill (judge r3 blockers)
BLOCK0: catalog deploy audit now uses the REAL AuditLogger contract
(auditLogger.log({action,resource,details,outcome,ip})) fire-and-forget
via Promise.resolve().catch — async audit failures can never fail the
deploy response (tested with a throwing audit backend).
BLOCK1: verify.sh targeted-jest block captures jest exit code explicitly
(set +e / rc / loud FAIL) — no silent termination under set -e.
BLOCK2: subdomain advertised by the App Selector modal is now FORWARDED
end-to-end (selector -> /deploys/catalog/deploy validated charset ->
bridge --subdomain -> CLI CatalogInstallOptions, implemented in
dc-catalog-2 r3); invalid subdomains 400 before the bridge (4 cases).
POLISH: boot-timeout SIGKILLs the child (no leaked process).
New r4 suite: 7 tests (audit shape success/failure, subdomain forward +
4 rejections). Full suite 157 suites / 3519 tests green.
2026-10-07 03:59:30 -07:00

95 lines
3.5 KiB
JavaScript

/**
* r4 additions: audit contract + subdomain forwarding (judge r3 blockers).
*/
const path = require('path');
const express = require('express');
const request = require('supertest');
const ROOT = path.resolve(__dirname, '..', '..');
process.env.SHIPDECK_BRIDGE_URL = 'http://127.0.0.1:19999';
process.env.SHIPDECK_BRIDGE_TOKEN_FILE = '';
const buildDeploys = require(path.join(ROOT, 'routes', 'deploys'));
const BRIDGE_URL = 'http://127.0.0.1:19999';
function makeApp(bridgeResponses, auditImpl) {
const calls = [];
const fetchT = async (url, opts) => {
const key = `${opts.method} ${url.replace(BRIDGE_URL, '')}`;
calls.push({ key, body: opts.body ? JSON.parse(opts.body) : null });
const r = bridgeResponses[key] || { status: 404, body: { ok: false, error: 'no stub' } };
return { status: r.status, json: async () => r.body };
};
const log = { info() {}, error() {}, warn() {} };
const audits = [];
const auditLogger = auditImpl || {
log: async (entry) => { audits.push(entry); },
};
const e = express();
e.use(express.json());
const asyncHandler = (fn) => async (req, res, next) => {
try { await fn(req, res, next); } catch (err) { next(err); }
};
e.use('/api/v1/deploys', buildDeploys({ asyncHandler, log, auditLogger, fetchT }));
return { app: e, calls, audits };
}
describe('DC-CATALOG-3 r4: audit contract + subdomain forwarding', () => {
it('success deploy audits via auditLogger.log({action,resource,details,outcome,ip})', async () => {
const { app, audits } = makeApp({
'POST /api/catalog/deploy': { status: 200, body: { ok: true, output: 'DEPLOYED plex', subdomain: 'plex' } },
});
await request(app)
.post('/api/v1/deploys/catalog/deploy')
.send({ app: 'plex', port: 32400, env: { PLEX_CLAIM: 'claim-x' } })
.expect(200);
expect(audits).toHaveLength(1);
expect(audits[0]).toMatchObject({
action: 'deploy.catalog',
resource: 'catalog-app:plex',
outcome: 'success',
});
expect(audits[0].details).toMatchObject({ app: 'plex', port: 32400, ok: true });
expect(typeof audits[0].ip).toBe('string');
});
it('failure deploy audits outcome=failure (never throws even if audit fails)', async () => {
const { app } = makeApp(
{ 'POST /api/catalog/deploy': { status: 400, body: { ok: false, error: 'requires env PLEX_CLAIM' } } },
{ log: async () => { throw new Error('audit backend down'); } }
);
const res = await request(app)
.post('/api/v1/deploys/catalog/deploy')
.send({ app: 'plex', port: 32400 })
.expect(502);
expect(res.body.error).toContain('PLEX_CLAIM');
});
it('valid subdomain is validated and forwarded to the bridge', async () => {
const { app, calls } = makeApp({
'POST /api/catalog/deploy': { status: 200, body: { ok: true, output: 'DEPLOYED' } },
});
await request(app)
.post('/api/v1/deploys/catalog/deploy')
.send({ app: 'whoami', port: 8100, subdomain: 'mybot' })
.expect(200);
expect(calls[0].body.subdomain).toBe('mybot');
});
it.each([
['uppercase', 'MyBot'],
['underscore', 'my_bot'],
['leading dash', '-bot'],
['too long', 'a'.repeat(64)],
])('invalid subdomain %s rejected with 400 before the bridge', async (_l, sub) => {
const { app, calls } = makeApp({});
await request(app)
.post('/api/v1/deploys/catalog/deploy')
.send({ app: 'whoami', port: 8100, subdomain: sub })
.expect(400);
expect(calls).toHaveLength(0);
});
});