- DC-160: /catalog/:appId read app.image/ports/env/volumes which live under app.docker.* — every detail response was empty. Now reads docker.* and surfaces defaultPort/subdomain/healthCheck/mediaMount/claimToken/difficulty/ setupInstructions for the deploy form. List/search use the templates own category field (old hand-rolled CATEGORY_MAP mis-categorized apps). - Shield (routes/shield.js + 5-line app.js mount) committed to source after weeks as prod-only drift; regression test asserts the mount exists. - New composition-root e2e suite: boots REAL createApp() on an ephemeral port, probes the historically-broken routes through the REAL mount table (wizard wiring, shield, catalog detail, deploys bridge) — the class of bug all 3 P7 dogfood findings belonged to. 5 tests. - Full suite 3499/3499 (155 suites); table-driven DC-160 test covers all 73 image templates.
99 lines
3.9 KiB
JavaScript
99 lines
3.9 KiB
JavaScript
/**
|
|
* Composition-root integration test — the "tests pass, prod breaks" cure.
|
|
*
|
|
* WHY: all three P7 dogfood bugs (wizard mount missing providers; coord
|
|
* client never attached; people deduped by id only) shipped to prod with the
|
|
* full unit suite green, because unit tests inject their own mocks instead
|
|
* of exercising the real app.js wiring. This suite boots the REAL createApp()
|
|
* on an ephemeral port and drives HTTP through the real mount table — the
|
|
* exact wiring that runs in production — for the routes that historically
|
|
* broke: /network/wizard, /security/shield, /catalog/:id, /deploys.
|
|
*
|
|
* Uses DATA_DIR to point the app at a throwaway data dir; no prod files are
|
|
* touched.
|
|
*/
|
|
const { spawn } = require('child_process');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const fs = require('fs');
|
|
|
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
|
|
// Boot the REAL app via its REAL entry contract (createApp + listen).
|
|
function boot(port) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, ['-e', `
|
|
process.env.DATA_DIR = ${JSON.stringify(DATA_DIR)};
|
|
const { createApp } = require(${JSON.stringify(path.join(ROOT, 'src', 'app'))});
|
|
// NOTE: the -e script resolves relative requires from CWD semantics;
|
|
// the absolute path above is required.
|
|
createApp().then((created) => {
|
|
const app = created.app || created;
|
|
const server = app.listen(${port}, '127.0.0.1', () => {
|
|
console.log('READY');
|
|
});
|
|
process.on('SIGTERM', () => server.close(() => process.exit(0)));
|
|
}).catch((e) => { console.error('BOOT-FAIL', e && e.message); process.exit(1); });
|
|
`], { cwd: ROOT, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
child.stdout.on('data', (d) => {
|
|
if (String(d).includes('READY')) resolve(child);
|
|
});
|
|
child.stderr.on('data', (d) => process.stderr.write('[boot] ' + d));
|
|
child.on('exit', (code) => reject(new Error('boot exited ' + code)));
|
|
setTimeout(() => reject(new Error('boot timeout')), 30000);
|
|
});
|
|
}
|
|
|
|
const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'dc-comp-root-'));
|
|
const PORT = 30000 + Math.floor(Math.random() * 20000);
|
|
let child;
|
|
|
|
async function get(p, expectAuth = true) {
|
|
const res = await fetch(`http://127.0.0.1:${PORT}${p}`, { redirect: 'manual' });
|
|
return { status: res.status, body: await res.text() };
|
|
}
|
|
|
|
describe('composition-root e2e (real createApp on ephemeral port)', () => {
|
|
beforeAll(async () => {
|
|
child = await boot(PORT);
|
|
}, 45000);
|
|
|
|
afterAll(async () => {
|
|
if (child) { child.kill('SIGTERM'); await new Promise((r) => child.on('exit', r)); }
|
|
fs.rmSync(DATA_DIR, { recursive: true, force: true });
|
|
});
|
|
|
|
it('boots the real app without throwing', () => {
|
|
expect(child.exitCode).toBeNull();
|
|
});
|
|
|
|
it('/network/wizard/state is mounted and wired (finding #1 regression)', async () => {
|
|
const r = await get('/api/v1/network/wizard/state');
|
|
// 401 = auth-gated but MOUNTED; the bug shipped 404/503-from-broken-mount.
|
|
expect([200, 401]).toContain(r.status);
|
|
});
|
|
|
|
it('/security/shield is mounted (prod-drift regression)', async () => {
|
|
const r = await get('/api/v1/security/shield');
|
|
// 401 = auth-gated but mounted; 200/503 = auth disabled/with snapshot.
|
|
// 404 would mean the prod-only drift regressed.
|
|
expect([200, 401, 503]).toContain(r.status);
|
|
});
|
|
|
|
it('/catalog/plex returns real image via app.docker.* (DC-160)', async () => {
|
|
const r = await get('/api/v1/catalog/plex');
|
|
// Auth-gated in the real composition root: assert mounted + gate, not 404.
|
|
expect([200, 401]).toContain(r.status);
|
|
if (r.status === 200) {
|
|
expect(r.body).toContain('plexinc/pms-docker');
|
|
} else {
|
|
expect(r.body).toContain('Authentication required');
|
|
}
|
|
});
|
|
|
|
it('/deploys bridge is mounted (opt-in feature flag)', async () => {
|
|
const r = await get('/api/v1/deploys/repos');
|
|
expect([200, 401, 501]).toContain(r.status);
|
|
});
|
|
});
|