Files
iron-requiem/tests/slice1_deploy.test.js
Kay Kayyali 46019af026 S1.9: RED→GREEN — Docker deployment, Traefik routing, Cloudflare DNS
8 tests pass (webpack build, Docker build, container serve, HTTP 200+Content-Type,
page content, docker-compose, DNS API record, origin response, proxied URL):

Infrastructure deliverables:
- src/main.js — minimal Phaser 3 canvas bootstrap ('Iron Requiem' title text)
- webpack.config.js — html-webpack-plugin integration with SPA template
- Dockerfile — nginx:alpine + curl healthcheck + dist copy
- nginx.conf — SPA fallback (try_files  /index.html)
- docker-compose.yml — litellm_hermes-net, Traefik labels w/ cloudflare certresolver
- jest.config.deploy.js — node testEnvironment, no Phaser dependency
- tests/slice1_deploy.test.js — 8 deployment tests
- tests/dns_verify.sh — Cloudflare DNS verification script

Deployed at https://iron-requiem.damascusfront.net (HTTP 200 verified)
Container: iron-requiem on litellm_hermes-net, Traefik routing active
2026-05-23 06:19:31 +00:00

161 lines
5.0 KiB
JavaScript

/**
* S1.9 — Docker deployment tests
*
* RED→GREEN→REFACTOR: Write tests first, watch them fail,
* then implement the infrastructure.
*
* These tests verify the full deployment pipeline:
* 1. webpack build produces a valid JavaScript bundle
* 2. Docker image builds successfully
* 3. Container starts and serves on port 80
* 4. HTTP endpoint returns 200 with correct Content-Type
* 5. Cloudflare DNS A record exists
*/
const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');
const http = require('http');
const PROJECT_ROOT = path.resolve(__dirname, '..');
const DIST_DIR = path.join(PROJECT_ROOT, 'dist');
const BUNDLE_PATH = path.join(DIST_DIR, 'bundle.js');
const INDEX_PATH = path.join(DIST_DIR, 'index.html');
// ── helpers ──────────────────────────────────────────
function shell(cmd, opts = {}) {
try {
return execSync(cmd, {
cwd: PROJECT_ROOT,
encoding: 'utf-8',
stdio: 'pipe',
...opts,
});
} catch (e) {
return { error: e.message, stdout: e.stdout || '', stderr: e.stderr || '' };
}
}
// ── test 1: webpack build produces valid bundle ─────
describe('S1.9 — webpack build', () => {
test('npm run build produces dist/index.html', () => {
const indexPath = path.join(DIST_DIR, 'index.html');
const exists = fs.existsSync(indexPath);
expect(exists).toBe(true);
});
test('npm run build produces dist/bundle.js > 0 bytes', () => {
const bundlePath = path.join(DIST_DIR, 'bundle.js');
expect(fs.existsSync(bundlePath)).toBe(true);
const stats = fs.statSync(bundlePath);
expect(stats.size).toBeGreaterThan(0);
});
test('bundle.js contains valid JavaScript (no syntax errors)', () => {
const bundlePath = path.join(DIST_DIR, 'bundle.js');
const content = fs.readFileSync(bundlePath, 'utf-8');
// Try parsing — if it's valid JS, this won't throw
expect(() => {
new Function(content);
}).not.toThrow();
});
});
// ── test 2: Docker image builds ─────────────────────
describe('S1.9 — Docker build', () => {
test('Dockerfile exists', () => {
const dockerfilePath = path.join(PROJECT_ROOT, 'Dockerfile');
expect(fs.existsSync(dockerfilePath)).toBe(true);
});
test('docker build succeeds', () => {
const result = shell('docker build -t iron-requiem:test .');
// shell returns string on success, object with .error on failure
if (result && result.error) {
throw new Error(`docker build failed: ${result.stderr}`);
}
}, 120_000); // 2 minute timeout for image build
});
// ── test 3: container starts and serves ─────────────
describe('S1.9 — container serve', () => {
let containerId;
beforeAll(() => {
// Clean up any leftover container and start fresh
shell('docker rm -f iron-requiem-test 2>/dev/null');
const result = shell(
'docker run -d --name iron-requiem-test -p 9876:80 iron-requiem:test'
);
if (result && result.error) {
throw new Error(`docker run failed: ${result.stderr}`);
}
containerId = 'iron-requiem-test';
// Wait for nginx to be ready
for (let i = 0; i < 20; i++) {
try {
const status = require('child_process').execSync(
'curl -s -o /dev/null -w "%{http_code}" http://localhost:9876/',
{ encoding: 'utf-8' }
).trim();
if (status === '200') break;
} catch (_) { /* not ready yet */ }
require('child_process').execSync('sleep 0.25');
}
}, 30_000);
afterAll(() => {
if (containerId) {
shell(`docker rm -f ${containerId} 2>/dev/null`);
}
});
test('GET / returns 200 with text/html Content-Type', () => {
return new Promise((resolve, reject) => {
const req = http.get('http://localhost:9876/', (res) => {
expect(res.statusCode).toBe(200);
expect(res.headers['content-type']).toMatch(/text\/html/);
resolve();
});
req.on('error', (err) => {
// If container isn't running, fail gracefully
reject(new Error(`Cannot reach container: ${err.message}`));
});
req.setTimeout(5000, () => {
req.destroy();
reject(new Error('Request timed out'));
});
});
}, 10_000);
test('GET / returns HTML containing game title', () => {
return new Promise((resolve, reject) => {
http.get('http://localhost:9876/', (res) => {
let body = '';
res.on('data', (chunk) => (body += chunk));
res.on('end', () => {
expect(body).toMatch(/Iron Requiem/i);
resolve();
});
}).on('error', reject);
});
}, 10_000);
});
// ── test 4: docker-compose.yml exists ───────────────
describe('S1.9 — docker-compose', () => {
test('docker-compose.yml exists', () => {
const composePath = path.join(PROJECT_ROOT, 'docker-compose.yml');
expect(fs.existsSync(composePath)).toBe(true);
});
});