// HTTP-Tests gegen die echte Express-App: Zugriffssperren, Bremsen, Fehlermeldungen. import { test, before, after } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import zlib from 'node:zlib'; const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rotatory-api-')); process.env.VCH_DATA_DIR = dataDir; process.env.EDIT_PASSWORD = 'test123'; process.env.TRUSTED_PROXY = 'none'; process.env.NEXTCLOUD_URL = ''; process.env.NEXTCLOUD_USER = ''; process.env.NEXTCLOUD_APP_PASSWORD = ''; process.env.APP_PASSWORD = ''; let server; let base; let cookie = ''; const json = (method, p, body, headers = {}) => fetch(base + p, { method, headers: { 'Content-Type': 'application/json', Cookie: cookie, ...headers }, body: body === undefined ? undefined : JSON.stringify(body) }); function tinyPng() { const raw = Buffer.from([0, 18, 46, 69]); const chunk = (type, body) => { const len = Buffer.alloc(4); len.writeUInt32BE(body.length); const crc = Buffer.alloc(4); crc.writeUInt32BE(zlib.crc32(Buffer.concat([Buffer.from(type), body])) >>> 0); return Buffer.concat([len, Buffer.from(type), body, crc]); }; const ihdr = Buffer.alloc(13); ihdr.writeUInt32BE(1, 0); ihdr.writeUInt32BE(1, 4); ihdr[8] = 8; ihdr[9] = 2; return Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), chunk('IHDR', ihdr), chunk('IDAT', zlib.deflateSync(raw)), chunk('IEND', Buffer.alloc(0))]); } const screenshot = 'data:image/png;base64,' + tinyPng().toString('base64'); before(async () => { const mod = await import('../server/index.js'); server = mod.app.listen(0, '127.0.0.1'); await new Promise((r) => server.once('listening', r)); base = `http://127.0.0.1:${server.address().port}`; }); after(() => { server.close(); fs.rmSync(dataDir, { recursive: true, force: true }); }); test('Betrachter: Lesen erlaubt, Schreiben und Bearbeiter-Reads gesperrt, auch in anderer Schreibweise', async () => { assert.equal((await fetch(base + '/health')).status, 200); assert.equal((await fetch(base + '/api/state')).status, 200); const st = await (await fetch(base + '/api/state')).json(); assert.deepEqual(st.auth, { protected: true, editor: false }); assert.ok(!('bugs' in st) && !('requests' in st), 'Betrachter sehen keine Meldungen und Anfragen'); assert.equal((await json('POST', '/api/persons', { firstName: 'A', lastName: 'B' })).status, 403); assert.equal((await json('PUT', '/api/settings', { bundesland: 'BY' })).status, 403); assert.equal((await fetch(base + '/api/nextcloud/calendars')).status, 403); assert.equal((await fetch(base + '/api/bugs/x/prompt')).status, 403, 'Prompt nur für Bearbeiter'); assert.equal((await json('PUT', '/API/settings', { bundesland: 'BY' })).status, 404, 'Großschreibung läuft ins Leere'); assert.equal((await json('DELETE', '/Api/bugs/x')).status, 404); assert.equal((await fetch(base + '/bugs/x.png')).status, 403); }); test('Fehlermeldung ohne Anmeldung, Bremse zählt auch abgelehnte Versuche', async () => { const bad = await json('POST', '/api/bugs', { text: '' }); assert.equal(bad.status, 400); const ok = await json('POST', '/api/bugs', { text: 'Knopf tut nichts', reporter: 'Anna', page: 'Kalender', viewport: '1×1', userAgent: 'Test', screenshot }); assert.equal(ok.status, 201); const { id } = await ok.json(); assert.match(id, /^[a-f0-9-]{36}$/); assert.equal((await fetch(base + `/bugs/${id}.png`)).status, 403, 'Screenshot nicht für Betrachter'); assert.equal((await fetch(base + `/api/bugs/${id}/prompt`)).status, 403); // 2 Versuche verbraucht; 8 weitere (auch ungültige) und der 11. wird gebremst for (let i = 0; i < 8; i += 1) await json('POST', '/api/bugs', { text: '' }); const limited = await json('POST', '/api/bugs', { text: 'noch einer' }); assert.equal(limited.status, 429); const spoofed = await json('POST', '/api/bugs', { text: 'mit falschem Header' }, { 'X-Forwarded-For': '10.9.8.7' }); assert.equal(spoofed.status, 429, 'X-Forwarded-For wird ohne vertrauten Proxy ignoriert'); }); test('Bearbeiter: anmelden, Meldungen sehen, Prompt lesen, Screenshot abrufen, erledigen, löschen', async () => { const wrong = await json('POST', '/api/auth/login', { password: 'falsch' }); assert.equal(wrong.status, 401); const login = await json('POST', '/api/auth/login', { password: 'test123' }); assert.equal(login.status, 200); cookie = login.headers.get('set-cookie').split(';')[0]; const st = await (await fetch(base + '/api/state', { headers: { Cookie: cookie } })).json(); assert.equal(st.auth.editor, true); assert.equal(st.bugs.length, 1); const bug = st.bugs[0]; assert.equal(bug.hasScreenshot, true); const prompt = await (await fetch(base + `/api/bugs/${bug.id}/prompt`, { headers: { Cookie: cookie } })).text(); assert.ok(prompt.includes('Knopf tut nichts') && prompt.includes('Anna')); const png = await fetch(base + `/bugs/${bug.id}.png`, { headers: { Cookie: cookie } }); assert.equal(png.status, 200); assert.equal(png.headers.get('content-type'), 'image/png'); assert.equal((await json('PUT', `/api/bugs/${bug.id}`, { status: 'erledigt' })).status, 200); assert.equal((await json('PUT', `/api/bugs/${bug.id}`, { status: 'quatsch' })).status, 400); const tpl = await json('PUT', '/api/settings', { bugPromptTemplate: 'Nur: {text}' }); assert.equal(tpl.status, 200); assert.equal(await (await fetch(base + `/api/bugs/${bug.id}/prompt`, { headers: { Cookie: cookie } })).text(), 'Nur: Knopf tut nichts'); assert.equal((await json('PUT', '/api/settings', { bugPromptTemplate: '' })).status, 200, 'leer stellt die Vorlage wieder her'); assert.ok((await (await fetch(base + `/api/bugs/${bug.id}/prompt`, { headers: { Cookie: cookie } })).text()).startsWith('Das ist ein Bug')); assert.equal((await json('DELETE', `/api/bugs/${bug.id}`)).status, 204); assert.equal((await fetch(base + `/bugs/${bug.id}.png`, { headers: { Cookie: cookie } })).status, 404); assert.equal(fs.existsSync(path.join(dataDir, 'bugs', `${bug.id}.png`)), false, 'PNG mit gelöscht'); }); test('Abmelden macht wieder zum Betrachter', async () => { const out = await json('POST', '/api/auth/logout', {}); assert.equal(out.status, 200); cookie = out.headers.get('set-cookie').split(';')[0]; assert.equal((await json('POST', '/api/persons', { firstName: 'A', lastName: 'B' })).status, 403); });