169 lines
11 KiB
JavaScript
169 lines
11 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
||
import { tmpdir } from 'node:os';
|
||
import { join } from 'node:path';
|
||
import { createJobs } from '../src/jobs.js';
|
||
import { validateJob } from '../src/validate.js';
|
||
|
||
function setup({ files, dockerImpl }) {
|
||
const root = mkdtempSync(join(tmpdir(), 'idx-'));
|
||
const config = { giteaUrl: 'https://git.x', giteaToken: 'tok_SECRET_123456', previewsRoot: join(root, 'previews'), workRoot: join(root, 'work'), cacheRoot: join(root, 'cache'), minFreeBytes: 1000, buildTimeoutMs: 900000 };
|
||
const git = async ({ dir }) => { for (const [f, c] of Object.entries(files)) { mkdirSync(join(dir, f, '..'), { recursive: true }); writeFileSync(join(dir, f), c); } mkdirSync(join(dir, '.git'), { recursive: true }); writeFileSync(join(dir, '.git', 'config'), 'x'); };
|
||
const docker = { run: async (args) => (dockerImpl ? dockerImpl(args) : { code: 0 }) };
|
||
const jobs = createJobs({ config, docker, git, mem: () => 1e10, fs: { chownR: async () => {} }, setTimer: () => {} });
|
||
return { config, jobs, out: join(config.previewsRoot, 'demo') };
|
||
}
|
||
const job = (o = {}) => ({ sub: 'demo', repo: 'own/rep', sha: 'abcdef1', branch: 'main', mode: 'auto', env: {}, ...o });
|
||
const wait = async (jobs, id) => { for (let i = 0; i < 400; i++) { const r = jobs.get(id); if (r.state === 'done' || r.state === 'failed') return r; await new Promise((r2) => setTimeout(r2, 5)); } throw new Error('timeout'); };
|
||
const workOf = (args) => args.find((a) => a.endsWith(':/work')).split(':')[0];
|
||
|
||
test('Doener-Fall: nur Doener_Nation_Website.html ⇒ index.html ist Byte-Kopie, Original bleibt', async () => {
|
||
const html = Buffer.from('<!doctype html><title>Döner</title>äö\n');
|
||
const s = setup({ files: { 'Doener_Nation_Website.html': html, '.webklar-preview.json': '{}' } });
|
||
const r = await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.equal(r.state, 'done');
|
||
assert.deepEqual(readFileSync(join(s.out, 'index.html')), html);
|
||
assert.deepEqual(readFileSync(join(s.out, 'Doener_Nation_Website.html')), html);
|
||
assert.equal(statSync(join(s.out, 'index.html')).mode & 0o777, 0o644);
|
||
});
|
||
|
||
test('vorhandene index.html bleibt unberührt', async () => {
|
||
const s = setup({ files: { 'index.html': 'ORIG', 'home.html': 'HOME' } });
|
||
await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'ORIG');
|
||
});
|
||
|
||
test('bevorzugter Name gewinnt, sonst größte Datei', async () => {
|
||
let s = setup({ files: { 'a.html': 'aaaa', 'Main.html': 'm', 'zz.html': 'zzzzzzzz' } });
|
||
await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'm');
|
||
s = setup({ files: { 'a.html': 'aaaa', 'b.htm': 'bbbbbbbbb', 'c.html': 'c' } });
|
||
await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'bbbbbbbbb');
|
||
});
|
||
|
||
test('index-Unterordner wird Webroot; fehlender Ordner ⇒ Repo-Wurzel', async () => {
|
||
let s = setup({ files: { 'public/index.html': 'PUB', 'x.html': 'root' } });
|
||
let r = await wait(s.jobs, s.jobs.submit(job({ index: 'public' })));
|
||
assert.equal(r.state, 'done');
|
||
assert.deepEqual(readdirSync(s.out), ['index.html']);
|
||
s = setup({ files: { 'x.html': 'root' } });
|
||
await wait(s.jobs, s.jobs.submit(job({ index: 'gibtsnicht' })));
|
||
assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'root');
|
||
});
|
||
|
||
test('kein HTML ⇒ Platzhalter mit Anzeigename (maskiert)', async () => {
|
||
const s = setup({ files: { 'readme.txt': 'x' } });
|
||
await wait(s.jobs, s.jobs.submit(job({ displayName: 'A & <B>' })));
|
||
const h = readFileSync(join(s.out, 'index.html'), 'utf8');
|
||
assert.match(h, /<h1>A & <B><\/h1>/);
|
||
assert.match(h, /<title>A & <B> – wird vorbereitet<\/title>/);
|
||
const s2 = setup({ files: { 'readme.txt': 'x' } });
|
||
await wait(s2.jobs, s2.jobs.submit(job()));
|
||
assert.match(readFileSync(join(s2.out, 'index.html'), 'utf8'), /<h1>demo<\/h1>/);
|
||
});
|
||
|
||
test('Build ohne index.html im Ausgabeordner ⇒ Quellstand statisch + Kandidat/Platzhalter', async () => {
|
||
const s = setup({
|
||
files: { 'package.json': JSON.stringify({ scripts: { build: 'x' } }), 'seite.html': 'SEITE' },
|
||
dockerImpl: (args) => { if (args.includes('build')) { mkdirSync(join(workOf(args), 'dist'), { recursive: true }); writeFileSync(join(workOf(args), 'dist', 'app.js'), 'x'); mkdirSync(join(workOf(args), 'node_modules'), { recursive: true }); writeFileSync(join(workOf(args), 'node_modules', 'm.js'), 'x'); } return { code: 0 }; },
|
||
});
|
||
const r = await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.equal(r.state, 'done'); assert.equal(r.result, 'static');
|
||
assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'SEITE');
|
||
assert.ok(!existsSync(join(s.out, 'node_modules')));
|
||
const s2 = setup({
|
||
files: { 'package.json': JSON.stringify({ scripts: { build: 'x' } }) },
|
||
dockerImpl: (args) => { if (args.includes('build')) mkdirSync(join(workOf(args), 'dist'), { recursive: true }); return { code: 0 }; },
|
||
});
|
||
await wait(s2.jobs, s2.jobs.submit(job({ displayName: 'Shop' })));
|
||
assert.match(readFileSync(join(s2.out, 'index.html'), 'utf8'), /<h1>Shop<\/h1>/);
|
||
});
|
||
|
||
test('Build-Ausgabe mit index.html wird unverändert veröffentlicht', async () => {
|
||
const s = setup({
|
||
files: { 'package.json': JSON.stringify({ scripts: { build: 'x' } }) },
|
||
dockerImpl: (args) => { if (args.includes('build')) { mkdirSync(join(workOf(args), 'dist'), { recursive: true }); writeFileSync(join(workOf(args), 'dist', 'index.html'), 'B'); writeFileSync(join(workOf(args), 'dist', 'o.html'), 'O'); } return { code: 0 }; },
|
||
});
|
||
const r = await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.equal(r.result, 'build');
|
||
assert.deepEqual(readdirSync(s.out).sort(), ['index.html', 'o.html']);
|
||
});
|
||
|
||
const BUILD_PKG = JSON.stringify({ scripts: { build: 'x' } });
|
||
const failBuild = (args) => ({ code: args.includes('build') ? 1 : 0 });
|
||
|
||
test('bestehender <sub>-Ordner + fehlgeschlagener Build ⇒ failed, alter Stand unverändert', async () => {
|
||
const s = setup({ files: { 'package.json': BUILD_PKG, 'x.html': 'QUELLE' }, dockerImpl: failBuild });
|
||
mkdirSync(s.out, { recursive: true }); writeFileSync(join(s.out, 'index.html'), 'ALT');
|
||
const r = await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.equal(r.state, 'failed');
|
||
assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'ALT');
|
||
});
|
||
|
||
test('erstes Deploy + fehlgeschlagener Build ⇒ Quellstand statisch (Kandidat), result static, Hinweis', async () => {
|
||
const s = setup({ files: { 'package.json': BUILD_PKG, 'x.html': 'QUELLE' }, dockerImpl: failBuild });
|
||
const r = await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.equal(r.state, 'done'); assert.equal(r.result, 'static'); assert.match(r.hint, /Build fehlgeschlagen/);
|
||
assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'QUELLE');
|
||
assert.ok(r.log.some((l) => /erstes Deploy/.test(l)));
|
||
});
|
||
|
||
test('erstes Deploy + Installationsfehler ⇒ statischer Quellstand', async () => {
|
||
const s = setup({ files: { 'package.json': BUILD_PKG }, dockerImpl: () => ({ code: 1 }) });
|
||
const r = await wait(s.jobs, s.jobs.submit(job({ displayName: 'Shop' })));
|
||
assert.equal(r.result, 'static');
|
||
assert.match(readFileSync(join(s.out, 'index.html'), 'utf8'), /<h1>Shop<\/h1>/);
|
||
});
|
||
|
||
test('R21: json type static + index.html + build-Skript ⇒ static, kein Container', async () => {
|
||
const s = setup({ files: { 'package.json': BUILD_PKG, 'index.html': 'S', '.webklar-preview.json': '{"type":"static"}' }, dockerImpl: () => { throw new Error('kein Container'); } });
|
||
const r = await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.equal(r.state, 'done'); assert.equal(r.result, 'static');
|
||
});
|
||
|
||
test('R21: json type node_build ⇒ build (auch mit index.html); kein Dev-Rückfall', async () => {
|
||
const mk = (extra) => setup({ files: { 'package.json': JSON.stringify({ scripts: { build: 'x', dev: 'y' } }), 'index.html': 'S', '.webklar-preview.json': '{"type":"node_build"}', ...extra },
|
||
dockerImpl: (args) => { if (args.includes('build')) { mkdirSync(join(workOf(args), 'dist'), { recursive: true }); writeFileSync(join(workOf(args), 'dist', 'index.html'), 'B'); } return { code: 0 }; } });
|
||
let s = mk();
|
||
let r = await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.equal(r.result, 'build'); assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'B');
|
||
s = setup({ files: { 'package.json': JSON.stringify({ scripts: { build: 'x', dev: 'y' } }), '.webklar-preview.json': '{"type":"node_build"}' }, dockerImpl: failBuild });
|
||
mkdirSync(s.out, { recursive: true });
|
||
r = await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.equal(r.state, 'failed');
|
||
});
|
||
|
||
test('R21: ohne json-Typ + index.html + build-Skript ⇒ build', async () => {
|
||
const s = setup({ files: { 'package.json': BUILD_PKG, 'index.html': 'S' }, dockerImpl: (args) => { if (args.includes('build')) { mkdirSync(join(workOf(args), 'dist'), { recursive: true }); writeFileSync(join(workOf(args), 'dist', 'index.html'), 'B'); } return { code: 0 }; } });
|
||
const r = await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.equal(r.result, 'build');
|
||
});
|
||
|
||
test('R21: Modus build aus dem Portal schlägt json type static', async () => {
|
||
const s = setup({ files: { 'package.json': BUILD_PKG, 'index.html': 'S', '.webklar-preview.json': '{"type":"static"}' }, dockerImpl: (args) => { if (args.includes('build')) { mkdirSync(join(workOf(args), 'dist'), { recursive: true }); writeFileSync(join(workOf(args), 'dist', 'index.html'), 'B'); } return { code: 0 }; } });
|
||
const r = await wait(s.jobs, s.jobs.submit(job({ mode: 'build' })));
|
||
assert.equal(r.result, 'build');
|
||
});
|
||
|
||
test('json index-Unterordner wird beim Veröffentlichen genutzt (static und Build-Rückfall)', async () => {
|
||
let s = setup({ files: { 'public/index.html': 'PUB', 'root.html': 'r', '.webklar-preview.json': '{"index":"public"}' } });
|
||
await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.deepEqual(readdirSync(s.out), ['index.html']);
|
||
s = setup({ files: { 'package.json': BUILD_PKG, 'public/home.html': 'H', '.webklar-preview.json': '{"index":"public"}' }, dockerImpl: failBuild });
|
||
const r = await wait(s.jobs, s.jobs.submit(job()));
|
||
assert.equal(r.result, 'static');
|
||
assert.equal(readFileSync(join(s.out, 'index.html'), 'utf8'), 'H');
|
||
assert.ok(!existsSync(join(s.out, 'package.json')));
|
||
});
|
||
|
||
test('validateJob: displayName optional, ≤100 Zeichen, keine Steuerzeichen', () => {
|
||
const good = { sub: 'a', repo: 'o/r', sha: 'abcdef1', branch: 'main' };
|
||
assert.equal(validateJob({ ...good, displayName: 'Döner Nation' }).job.displayName, 'Döner Nation');
|
||
assert.equal('displayName' in validateJob(good).job, false);
|
||
assert.equal(validateJob({ ...good, displayName: 'x'.repeat(101) }).ok, false);
|
||
assert.equal(validateJob({ ...good, displayName: 'a\nb' }).ok, false);
|
||
assert.equal(validateJob({ ...good, displayName: 5 }).ok, false);
|
||
});
|