import { execFileSync } from 'node:child_process'; import { randomBytes } from 'node:crypto'; import { readFile } from 'node:fs/promises'; import assert from 'node:assert/strict'; // Isolated disposable Compose project. Never targets the user's deployment or volumes. const project = `hamham-check-${randomBytes(4).toString('hex')}`; const port = process.env.VERIFY_PORT ?? '3200'; const origin = `http://localhost:${port}`; const password = randomBytes(30).toString('base64url'); const env = { ...process.env, POSTGRES_PASSWORD: randomBytes(32).toString('hex'), HOST_PORT: port, APP_ORIGIN: origin, COOKIE_SECURE: 'false', }; const compose = (args: string[], input?: string | Buffer) => execFileSync('docker', ['compose', '-p', project, ...args], { env, input, maxBuffer: 32 * 1024 * 1024, stdio: input === undefined ? ['ignore', 'pipe', 'inherit'] : ['pipe', 'pipe', 'inherit'], }); let cookie = ''; async function request(path: string, method = 'GET', body?: unknown) { const response = await fetch(`${origin}/api/${path}`, { method, headers: { Origin: origin, Cookie: cookie, Connection: 'close', ...(body === undefined ? {} : { 'Content-Type': 'application/json' }), }, body: body === undefined ? undefined : JSON.stringify(body), }); assert.ok( response.ok, `${method} ${path}: ${response.status} ${response.ok ? '' : await response.text()}`, ); if (response.headers.get('set-cookie')) cookie = response.headers.get('set-cookie')!.split(';')[0]; return response; } try { console.log(`Compose-Integrationstest: ${project} (${origin})`); console.log(compose(['up', '-d', '--build', '--wait', '--wait-timeout', '240']).toString()); assert.equal(compose(['exec', '-T', 'app', 'id', '-u']).toString().trim(), '1000'); console.log( compose( ['exec', '-T', 'app', 'npm', 'run', 'admin:create', '--', 'dockercheck'], `${password}\n`, ).toString(), ); await request('auth/login', 'POST', { username: 'dockercheck', password }); const raw = await readFile('public/examples/kochbuch.json', 'utf8'); const recipes = await ( await request('import', 'POST', { raw, format: 'json', preview: false }) ).json(); assert.equal(recipes.length, 3); const imageResponse = await fetch(`${origin}/api/media`, { method: 'POST', headers: { Origin: origin, Cookie: cookie, Connection: 'close', 'Content-Type': 'image/jpeg' }, body: await readFile('public/images/garden.jpg'), }); assert.equal(imageResponse.status, 201); const { url: image } = await imageResponse.json(); const before = await fetch(`${origin}${image}`, { headers: { Connection: 'close' } }).then((r) => r.arrayBuffer(), ); await request(`recipes/${recipes[0].id}`, 'PUT', { ...recipes[0], image }); assert.equal((await (await request('export')).json()).recipes.length, 3); console.log('Admin-CLI, Login, Import, Bild-Upload, Rezeptänderung und Export: OK'); console.log(compose(['restart']).toString()); console.log(compose(['up', '-d', '--wait', '--wait-timeout', '120']).toString()); const after = await fetch(`${origin}${image}`, { headers: { Connection: 'close' } }).then((r) => r.arrayBuffer(), ); assert.deepEqual(Buffer.from(after), Buffer.from(before)); assert.equal((await (await request('recipes')).json()).length, 3); console.log('Persistenz nach Neustart: Datenbank, Sitzung und Bildbytes OK'); const dump = compose(['exec', '-T', 'db', 'pg_dump', '-U', 'hamham', '-d', 'hamham', '-Fc']); compose(['exec', '-T', 'db', 'createdb', '-U', 'hamham', 'hamham_restore']); compose(['exec', '-T', 'db', 'pg_restore', '-U', 'hamham', '-d', 'hamham_restore'], dump); assert.equal( compose([ 'exec', '-T', 'db', 'psql', '-U', 'hamham', '-d', 'hamham_restore', '-Atc', 'SELECT count(*) FROM recipes', ]) .toString() .trim(), '3', ); console.log('PostgreSQL-Backup/Restore und wiederholte Migrationen: OK'); console.log(compose(['ps']).toString()); } finally { // Only the unique verification project created above is removed, including its test volumes. compose(['down', '-v', '--remove-orphans']); }