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
161 lines
5.0 KiB
JavaScript
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);
|
|
});
|
|
});
|