232 lines
13 KiB
JavaScript
232 lines
13 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { injectAnalytics, stripComments } from '../src/analytics-inject.js';
|
|
import { validateJob } from '../src/validate.js';
|
|
import { loadConfig } from '../src/config.js';
|
|
import { createJobs } from '../src/jobs.js';
|
|
|
|
const BASE = 'https://project.webklar.com/a/';
|
|
const KEY = 'abcdef0123456789abcd';
|
|
const A = (o = {}) => ({ siteKey: KEY, color: '#2b6cb0', privacy: { company: 'Muster GmbH', address: 'Weg 1, 12345 Ort', email: 'info@muster.de' }, ...o });
|
|
const TAG = `<script data-wk-analytics src="${BASE}w.js" data-site="${KEY}" data-color="#2b6cb0" defer></script>`;
|
|
const dir = () => mkdtempSync(join(tmpdir(), 'inj-'));
|
|
const put = (d, f, c) => { mkdirSync(join(d, f, '..'), { recursive: true }); writeFileSync(join(d, f), c); };
|
|
const get = (d, f) => readFileSync(join(d, f), 'utf8');
|
|
const count = (s, sub) => s.split(sub).length - 1;
|
|
|
|
test('mehrere Dateien inkl. Unterordner: Tag vor </head>, node_modules/Punktordner übersprungen', async () => {
|
|
const d = dir();
|
|
put(d, 'index.html', '<html><head><title>x</title></head><body>a</body></html>');
|
|
put(d, 'sub/seite.htm', '<head></head><p>b</p>');
|
|
put(d, 'node_modules/p/x.html', '<head></head>');
|
|
put(d, '.hidden/x.html', '<head></head>');
|
|
await injectAnalytics(d, A(), { baseUrl: BASE });
|
|
assert.equal(get(d, 'index.html'), `<html><head><title>x</title>${TAG}</head><body>a</body></html>`);
|
|
assert.equal(get(d, 'sub/seite.htm'), `<head>${TAG}</head><p>b</p>`);
|
|
assert.equal(get(d, 'node_modules/p/x.html'), '<head></head>');
|
|
assert.equal(get(d, '.hidden/x.html'), '<head></head>');
|
|
});
|
|
|
|
test('zweimal ausführen: kein Doppel; alter Tag wird ersetzt', async () => {
|
|
const d = dir();
|
|
put(d, 'index.html', '<head></head>');
|
|
await injectAnalytics(d, A(), { baseUrl: BASE });
|
|
await injectAnalytics(d, A(), { baseUrl: BASE });
|
|
assert.equal(count(get(d, 'index.html'), 'data-wk-analytics'), 1);
|
|
put(d, 'b.html', '<head><script data-wk-analytics src="https://alt/w.js" data-site="x" defer></script></head><body></body>');
|
|
await injectAnalytics(d, A(), { baseUrl: BASE });
|
|
assert.equal(get(d, 'b.html'), `<head>${TAG}</head><body></body>`);
|
|
});
|
|
|
|
test('ohne head: nach <body …>; ohne body: am Dateianfang; großes </HEAD>', async () => {
|
|
const d = dir();
|
|
put(d, 'a.html', '<body class="x">hi</body>');
|
|
put(d, 'b.html', '<p>nur text</p>');
|
|
put(d, 'c.html', '<HEAD><TITLE>x</TITLE></HEAD><BODY>y</BODY>');
|
|
await injectAnalytics(d, A(), { baseUrl: BASE });
|
|
assert.equal(get(d, 'a.html'), `<body class="x">${TAG}hi</body>`);
|
|
assert.equal(get(d, 'b.html'), `${TAG}<p>nur text</p>`);
|
|
assert.equal(get(d, 'c.html'), `<HEAD><TITLE>x</TITLE>${TAG}</HEAD><BODY>y</BODY>`);
|
|
});
|
|
|
|
test('Nicht-UTF8-Bytes bleiben erhalten', async () => {
|
|
const d = dir();
|
|
const before = Buffer.concat([Buffer.from('<head>'), Buffer.from([0xe4, 0xf6, 0xfc, 0xff, 0x80]), Buffer.from('</head>')]);
|
|
writeFileSync(join(d, 'l.html'), before);
|
|
await injectAnalytics(d, A(), { baseUrl: BASE });
|
|
const after = readFileSync(join(d, 'l.html'));
|
|
assert.deepEqual(after, Buffer.concat([Buffer.from('<head>'), Buffer.from([0xe4, 0xf6, 0xfc, 0xff, 0x80]), Buffer.from(TAG), Buffer.from('</head>')]));
|
|
});
|
|
|
|
test('Datei > 5 MB wird übersprungen (Logzeile)', async () => {
|
|
const d = dir();
|
|
const big = Buffer.alloc(5 * 1024 * 1024 + 1, 0x20); big.write('<head></head>', 0);
|
|
writeFileSync(join(d, 'gross.html'), big);
|
|
const lines = [];
|
|
await injectAnalytics(d, A(), { baseUrl: BASE, onLine: (l) => lines.push(l) });
|
|
assert.deepEqual(readFileSync(join(d, 'gross.html')), big);
|
|
assert.ok(lines.some((l) => l.includes('gross.html') && /5 MB/.test(l)));
|
|
});
|
|
|
|
test('Datenschutzseite: Platzhalter maskiert, Skript auch dort, Pflichtabschnitte', async () => {
|
|
const d = dir();
|
|
put(d, 'index.html', '<head></head>');
|
|
await injectAnalytics(d, A({ privacy: { company: '<script>alert(1)</script> & Co', address: 'A "B" <C>', email: "x'y@z.de" } }), { baseUrl: BASE });
|
|
const p = get(d, 'datenschutz-analyse.html');
|
|
assert.ok(p.includes('<script>alert(1)</script> & Co'));
|
|
assert.ok(!p.includes('<script>alert(1)'));
|
|
assert.ok(p.includes('A "B" <C>') && p.includes('x'y@z.de'));
|
|
assert.ok(!p.includes('{{'));
|
|
assert.equal(count(p, 'data-wk-analytics'), 1);
|
|
for (const s of ['Verantwortlicher', 'Zweck', 'Einwilligung', 'Rechtsgrundlage', 'Auftragsverarbeiter', 'Hetzner', 'Speicherdauer', 'Cookie-Einstellungen', 'Rechte']) assert.ok(p.includes(s), s);
|
|
});
|
|
|
|
test('ohne analytics: nichts geändert, keine Seite', async () => {
|
|
const d = dir();
|
|
put(d, 'index.html', '<head></head>');
|
|
await injectAnalytics(d, undefined, { baseUrl: BASE });
|
|
assert.equal(get(d, 'index.html'), '<head></head>');
|
|
assert.deepEqual(readdirSync(d), ['index.html']);
|
|
});
|
|
|
|
test('ungültige Eingaben werfen (siteKey, Farbe, Basis)', async () => {
|
|
const d = dir(); put(d, 'index.html', '<head></head>');
|
|
await assert.rejects(injectAnalytics(d, A({ siteKey: 'ABC' }), { baseUrl: BASE }));
|
|
await assert.rejects(injectAnalytics(d, A({ color: 'red' }), { baseUrl: BASE }));
|
|
await assert.rejects(injectAnalytics(d, A(), { baseUrl: 'http://x/a/' }));
|
|
await assert.rejects(injectAnalytics(d, A(), { baseUrl: 'https://x/a' }));
|
|
assert.equal(get(d, 'index.html'), '<head></head>');
|
|
});
|
|
|
|
test('validateJob: analytics streng, Weiterreichen in den Job', () => {
|
|
const base = { sub: 'demo', repo: 'JUSN/demo', sha: 'abcdef1', branch: 'main' };
|
|
const ok = validateJob({ ...base, analytics: { ...A(), extra: 1 } });
|
|
assert.equal(ok.ok, true);
|
|
assert.deepEqual(ok.job.analytics, A());
|
|
assert.equal(validateJob(base).job.analytics, undefined);
|
|
const nul = validateJob({ ...base, analytics: null });
|
|
assert.equal(nul.ok, true); assert.equal(nul.job.analytics, undefined);
|
|
const noAddr = validateJob({ ...base, analytics: A({ privacy: { company: 'F', email: 'e@x.de' } }) });
|
|
assert.equal(noAddr.ok, true); assert.equal(noAddr.job.analytics.privacy.address, '');
|
|
assert.equal(validateJob({ ...base, analytics: A({ privacy: { company: 'F', email: '' } }) }).ok, false);
|
|
assert.equal(validateJob({ ...base, analytics: A({ privacy: { email: 'e@x.de' } }) }).ok, false);
|
|
const bad = [
|
|
A({ siteKey: 'kurz' }), A({ siteKey: 'A'.repeat(20) }), A({ color: '#ABCDEF' }), A({ color: '#abc' }),
|
|
A({ privacy: undefined }), A({ privacy: { ...A().privacy, company: 'x'.repeat(201) } }),
|
|
A({ privacy: { ...A().privacy, address: 'x'.repeat(301) } }), A({ privacy: { ...A().privacy, email: 'x'.repeat(201) } }),
|
|
A({ privacy: { ...A().privacy, company: 'a\nb' } }), A({ privacy: { ...A().privacy, email: 5 } }), 'x', [],
|
|
];
|
|
for (const a of bad) { const r = validateJob({ ...base, analytics: a }); assert.equal(r.ok, false, JSON.stringify(a)); assert.match(r.error, /Analyse/); }
|
|
});
|
|
|
|
test('loadConfig: analyticsPublicBase Vorgabe, Override, ungültig wirft', () => {
|
|
assert.equal(loadConfig({}).analyticsPublicBase, BASE);
|
|
assert.equal(loadConfig({ ANALYTICS_PUBLIC_BASE: 'https://x.de/q/' }).analyticsPublicBase, 'https://x.de/q/');
|
|
assert.throws(() => loadConfig({ ANALYTICS_PUBLIC_BASE: 'http://x.de/q/' }));
|
|
assert.throws(() => loadConfig({ ANALYTICS_PUBLIC_BASE: 'https://x.de/q' }));
|
|
});
|
|
|
|
// ---- Integration über publish ----
|
|
function setup(cfg = {}, fsOver = {}) {
|
|
const root = mkdtempSync(join(tmpdir(), 'injjobs-'));
|
|
const config = { giteaUrl: 'https://git.x', giteaToken: 't', previewsRoot: join(root, 'previews'), workRoot: join(root, 'work'), cacheRoot: join(root, 'cache'), minFreeBytes: 1000, buildTimeoutMs: 900000, analyticsPublicBase: BASE, ...cfg };
|
|
mkdirSync(config.previewsRoot, { recursive: true });
|
|
const git = async ({ dir: d }) => { put(d, 'index.html', '<html><head></head><body>NEU</body></html>'); put(d, 'x/y.html', '<p>y</p>'); };
|
|
const jobs = createJobs({ config, docker: { run: async () => ({ code: 0 }) }, git, mem: () => 1e10, fs: { chownR: async () => {}, ...fsOver } });
|
|
return { config, jobs };
|
|
}
|
|
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'); };
|
|
|
|
test('publish: Job mit analytics injiziert im veröffentlichten Ordner', async () => {
|
|
const s = setup();
|
|
const r = await wait(s.jobs, s.jobs.submit(job({ analytics: A() })));
|
|
assert.equal(r.state, 'done');
|
|
const t = join(s.config.previewsRoot, 'demo');
|
|
assert.ok(get(t, 'index.html').includes(TAG));
|
|
assert.ok(get(t, 'x/y.html').startsWith(TAG));
|
|
assert.ok(existsSync(join(t, 'datenschutz-analyse.html')));
|
|
assert.deepEqual(readdirSync(s.config.previewsRoot), ['demo']);
|
|
});
|
|
|
|
test('publish: Job ohne analytics bleibt unverändert', async () => {
|
|
const s = setup();
|
|
const r = await wait(s.jobs, s.jobs.submit(job()));
|
|
assert.equal(r.state, 'done');
|
|
const t = join(s.config.previewsRoot, 'demo');
|
|
assert.equal(get(t, 'index.html'), '<html><head></head><body>NEU</body></html>');
|
|
assert.ok(!existsSync(join(t, 'datenschutz-analyse.html')));
|
|
});
|
|
|
|
test('publish: Fehler beim Einsetzen ⇒ failed, alter Ordner bleibt (V4)', async () => {
|
|
const s = setup({}, { readFile: async () => { throw new Error('kaputt'); } });
|
|
const t = join(s.config.previewsRoot, 'demo');
|
|
mkdirSync(t); writeFileSync(join(t, 'index.html'), 'ALT');
|
|
const r = await wait(s.jobs, s.jobs.submit(job({ analytics: A() })));
|
|
assert.equal(r.state, 'failed');
|
|
assert.equal(get(t, 'index.html'), 'ALT');
|
|
assert.deepEqual(readdirSync(s.config.previewsRoot), ['demo']);
|
|
});
|
|
|
|
test('Dev-Modus (prepareWorkspace) fasst Dateien nicht an', async () => {
|
|
const s = setup();
|
|
const { workDir } = await s.jobs.prepareWorkspace(job({ analytics: A() }), { workDir: join(s.config.workRoot, 'dev', 'x') });
|
|
assert.equal(get(workDir, 'index.html'), '<html><head></head><body>NEU</body></html>');
|
|
assert.ok(!existsSync(join(workDir, 'datenschutz-analyse.html')));
|
|
});
|
|
|
|
test('leere Adresse: keine leere Zeile, kein Platzhalterrest', async () => {
|
|
const d = dir(); put(d, 'index.html', '<head></head>');
|
|
await injectAnalytics(d, A({ privacy: { company: 'Muster GmbH', address: '', email: 'info@muster.de' } }), { baseUrl: BASE });
|
|
const p = get(d, 'datenschutz-analyse.html');
|
|
assert.ok(p.includes('Muster GmbH<br>E-Mail:'));
|
|
assert.ok(!p.includes('{{') && !p.includes('<br><br>'));
|
|
});
|
|
|
|
test('Marker in script-String, Kommentar, template: Tag landet an der echten Stelle', async () => {
|
|
const d = dir();
|
|
put(d, 'a.html', '<head><script>var t="</head>";</script></head><body></body>');
|
|
put(d, 'b.html', '<!-- </head> --><head></head>');
|
|
put(d, 'c.html', '<template><body></template><p>x</p>');
|
|
put(d, 'd.html', '<style>/* </head> */</style><head></head>');
|
|
await injectAnalytics(d, A(), { baseUrl: BASE });
|
|
assert.equal(get(d, 'a.html'), `<head><script>var t="</head>";</script>${TAG}</head><body></body>`);
|
|
assert.equal(get(d, 'b.html'), `<!-- </head> --><head>${TAG}</head>`);
|
|
assert.equal(get(d, 'c.html'), `${TAG}<template><body></template><p>x</p>`);
|
|
assert.equal(get(d, 'd.html'), `<style>/* </head> */</style><head>${TAG}</head>`);
|
|
});
|
|
|
|
test('alter Tag im Kommentar wird nicht ersetzt; echter wird genau einmal eingesetzt', async () => {
|
|
const d = dir();
|
|
const old = '<script data-wk-analytics src="https://alt/w.js"></script>';
|
|
put(d, 'a.html', `<head><!-- ${old} --></head>`);
|
|
await injectAnalytics(d, A(), { baseUrl: BASE });
|
|
assert.equal(get(d, 'a.html'), `<head><!-- ${old} -->${TAG}</head>`);
|
|
});
|
|
|
|
test('5-MB-Grenze: genau 5 MB wird bearbeitet', async () => {
|
|
const d = dir();
|
|
const b = Buffer.alloc(5 * 1024 * 1024, 0x20); b.write('<head></head>', 0);
|
|
writeFileSync(join(d, 'g.html'), b);
|
|
await injectAnalytics(d, A(), { baseUrl: BASE });
|
|
assert.ok(get(d, 'g.html').includes('data-wk-analytics'));
|
|
});
|
|
|
|
test('validateJob: Adresse nur Leerraum ⇒ leer, sonst getrimmt', () => {
|
|
const base = { sub: 'demo', repo: 'JUSN/demo', sha: 'abcdef1', branch: 'main' };
|
|
const r = validateJob({ ...base, analytics: A({ privacy: { company: 'F', email: 'e@x.de', address: ' ' } }) });
|
|
assert.equal(r.job.analytics.privacy.address, '');
|
|
const r2 = validateJob({ ...base, analytics: A({ privacy: { company: 'F', email: 'e@x.de', address: ' Weg 1 ' } }) });
|
|
assert.equal(r2.job.analytics.privacy.address, 'Weg 1');
|
|
});
|
|
|
|
test('Zusatzseite enthält keine HTML-Kommentare', async () => {
|
|
const d = dir();
|
|
await injectAnalytics(d, A(), { baseUrl: BASE });
|
|
assert.ok(!get(d, 'datenschutz-analyse.html').includes('<!--'));
|
|
assert.equal(stripComments('a<!-- Prüfpunkt\nx -->b<!--y-->c'), 'abc');
|
|
});
|