78 lines
3.5 KiB
JavaScript
78 lines
3.5 KiB
JavaScript
// Real image boundary: catches missing native modules, proto assets and migrations.
|
|
const assert = require('node:assert/strict');
|
|
const { execFileSync } = require('node:child_process');
|
|
const { randomUUID } = require('node:crypto');
|
|
|
|
const image = process.argv[2];
|
|
assert.ok(image, 'Pass a built backend image reference');
|
|
const name = `wealthledger-smoke-${randomUUID()}`;
|
|
// eslint-disable-next-line sonarjs/no-os-command-from-path -- Developer-only command uses the operator PATH and fixed executable/arguments; no request input.
|
|
const docker = (...args) => execFileSync('docker', args, { encoding: 'utf8' }).trim();
|
|
const environment = [
|
|
'-e', 'DATABASE_URL=file:/data/wealthledger.db',
|
|
'-e', 'FRONTEND_ORIGIN=http://localhost',
|
|
'-e', 'JWT_SECRET=disposable-image-smoke-secret-at-least-32-characters',
|
|
'-e', 'TOKEN_ENCRYPTION_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=',
|
|
'-e', 'LOG_LEVEL=error',
|
|
];
|
|
|
|
async function ready() {
|
|
for (let attempt = 0; attempt < 90; attempt++) {
|
|
const state = JSON.parse(docker('inspect', name, '--format', '{{json .State}}'));
|
|
assert.equal(state.Running, true, 'Image exited before readiness');
|
|
if (state.Health.Status === 'healthy') return;
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
}
|
|
throw new Error('Image readiness timed out');
|
|
}
|
|
|
|
(async () => {
|
|
docker('volume', 'create', name);
|
|
try {
|
|
assert.throws(() => docker('run', '--rm', '--read-only', image),
|
|
'Missing runtime configuration must prevent startup');
|
|
const migrate = () => docker('run', '--rm', '--read-only', '-v', `${name}:/data`,
|
|
...environment, image, 'node', 'node_modules/prisma/build/index.js', 'migrate', 'deploy');
|
|
migrate();
|
|
docker('run', '-d', '--name', name, '--read-only', '-v', `${name}:/data`, ...environment, image);
|
|
await ready();
|
|
assert.deepEqual(JSON.parse(docker('inspect', name, '--format', '{{json .HostConfig.PortBindings}}')), {});
|
|
docker('exec', name, 'node', '-e', `
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
assert.notEqual(process.getuid(), 0);
|
|
assert.equal(fs.existsSync('src'), false);
|
|
assert.equal(fs.existsSync('.env'), false);
|
|
assert.equal(fs.existsSync('node_modules/typescript'), false);
|
|
assert.equal(fs.existsSync('node_modules/@nestjs/cli'), false);
|
|
fs.accessSync('dist/src/integrations/tbank/proto/contracts/users.proto');
|
|
fetch('http://localhost:3000/api/v1/auth/register', {
|
|
method: 'POST', headers: {'content-type':'application/json'},
|
|
body: JSON.stringify({email:'image-smoke@example.test',password:'fictional-image-smoke-password'})
|
|
}).then(async r => {assert.equal(r.status, 200); await r.json()}).catch(e => {console.error(e); process.exitCode=1});
|
|
`);
|
|
docker('stop', name);
|
|
migrate();
|
|
docker('start', name);
|
|
await ready();
|
|
docker('exec', name, 'node', '-e', `
|
|
const assert = require('node:assert/strict');
|
|
const db = require('better-sqlite3')('/data/wealthledger.db');
|
|
assert.equal(db.prepare('SELECT count(*) AS n FROM User').get().n, 1);
|
|
assert.equal(db.prepare('SELECT count(*) AS n FROM Session').get().n, 1);
|
|
assert.equal(db.pragma('integrity_check', {simple:true}), 'ok');
|
|
db.close();
|
|
`);
|
|
console.log('PASS image: fresh/existing migration, non-root read-only runtime, auth, persistence, proto assets');
|
|
} finally {
|
|
try {
|
|
docker('rm', '-f', name);
|
|
} finally {
|
|
docker('volume', 'rm', name);
|
|
}
|
|
}
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|