// Geheimer Vorschau-Link: Link erzeugen, einloesen (Cookie + Weiterleitung), pruefen, abschalten import { test, before, after } from 'node:test' import assert from 'node:assert/strict' import express from 'express' import { startFakeAppwrite } from './helpers/fakeAppwrite.js' const ready = { subdomain: 'kunde-a', projectType: 'preview', provisioningStatus: 'deployed', customerId: 'c1' } let aw, server, baseUrl, svc before(async () => { aw = await startFakeAppwrite({ seed: { websiteProjects: [ { $id: 'p1', ...ready }, { $id: 'p2', ...ready, subdomain: 'baut-noch', provisioningStatus: 'pending', shareToken: 'Pending_Token_123456789' }, { $id: 'p3', projectName: 'ohne Subdomain', subdomain: '' }, { $id: 'p4', ...ready, subdomain: 'archiv', archived: true, shareToken: 'Archiv_Token_1234567890' }, ], }, }) Object.assign(process.env, { APPWRITE_ENDPOINT: aw.endpoint, APPWRITE_API_KEY: 'test-key', PREVIEW_SESSION_SECRET: 'x'.repeat(40) }) svc = await import('../server/services/vorschauLink.js') const { vorschauLinkSeite, vorschauLinkPruefung } = await import('../server/routes/vorschauLink.js') const app = express() app.use('/vorschau', vorschauLinkSeite()) app.use('/api/vorschau-link', vorschauLinkPruefung()) server = await new Promise((r) => { const s = app.listen(0, '127.0.0.1', () => r(s)) }) baseUrl = `http://127.0.0.1:${server.address().port}` }) after(async () => { server.closeAllConnections() await new Promise((r) => server.close(r)) await aw.close() }) const doc = (id) => aw.all('websiteProjects').find((d) => d.$id === id) const pruefen = (sub, token) => fetch(`${baseUrl}/api/vorschau-link/pruefen?subdomain=${sub}`, { headers: token ? { 'x-vorschau-token': token } : {} }) test('Token: zufaellig, URL-tauglich, lang genug', () => { const a = svc.newShareToken() assert.match(a, /^[A-Za-z0-9_-]{24}$/) assert.notEqual(a, svc.newShareToken()) assert.equal(svc.cleanToken(a), a) assert.equal(svc.cleanToken('kurz'), '') assert.equal(svc.cleanToken('../../etc/passwd/xxxxxxxxxxxxxxx'), '') assert.equal(svc.cleanSubdomain('Kunde-A'), 'kunde-a') assert.equal(svc.cleanSubdomain('-x'), '') assert.equal(svc.cleanSubdomain('a.b'), '') }) test('Link erzeugen speichert den Token und baut die URL', async () => { const r = await svc.renewShareLink('p1') assert.equal(doc('p1').shareToken, r.shareToken) assert.equal(r.url, `https://project.webklar.com/vorschau/kunde-a/${r.shareToken}`) }) test('Ohne Subdomain gibt es keinen Link (409), unbekanntes Projekt 404', async () => { await assert.rejects(svc.renewShareLink('p3'), (e) => e.status === 409) await assert.rejects(svc.renewShareLink('gibt-es-nicht'), (e) => e.status === 404) }) test('Link einloesen: Cookie nur fuer diese Subdomain, Weiterleitung auf die Preview', async () => { const { shareToken } = await svc.renewShareLink('p1') const res = await fetch(`${baseUrl}/vorschau/kunde-a/${shareToken}`, { redirect: 'manual' }) assert.equal(res.status, 302) assert.equal(res.headers.get('location'), 'https://kunde-a.project.webklar.com/') const cookie = res.headers.get('set-cookie') assert.match(cookie, new RegExp(`^wk_vorschau_kunde-a=${shareToken};`)) assert.match(cookie, /Domain=\.project\.webklar\.com/) assert.match(cookie, /HttpOnly/) assert.match(cookie, /SameSite=Lax/) assert.equal(res.headers.get('cache-control'), 'no-store') }) test('Falscher Token, fremde Subdomain oder Muell: 404-Seite ohne Cookie', async () => { const { shareToken } = await svc.renewShareLink('p1') for (const path of [ `/vorschau/kunde-a/${shareToken.slice(0, -1)}X`, `/vorschau/archiv/${shareToken}`, `/vorschau/kunde-a/kurz`, `/vorschau/KUNDE-A.evil/${shareToken}`, ]) { const res = await fetch(baseUrl + path, { redirect: 'manual' }) assert.equal(res.status, 404, path) assert.equal(res.headers.get('set-cookie'), null, path) assert.match(await res.text(), /nicht \(mehr\) gültig/) } }) test('Archiviertes Projekt: Link gilt nicht', async () => { const res = await pruefen('archiv', 'Archiv_Token_1234567890') assert.equal(res.status, 403) }) test('Preview noch nicht fertig: 503-Hinweis, Pruefung sagt not_ready', async () => { const res = await fetch(`${baseUrl}/vorschau/baut-noch/Pending_Token_123456789`, { redirect: 'manual' }) assert.equal(res.status, 503) assert.equal(res.headers.get('set-cookie'), null) const p = await pruefen('baut-noch', 'Pending_Token_123456789') assert.deepEqual(await p.json(), { allowed: false, reason: 'not_ready' }) }) test('Pruefung: richtiger Token 200, sonst 403', async () => { const { shareToken } = await svc.renewShareLink('p1') let res = await pruefen('kunde-a', shareToken) assert.equal(res.status, 200) assert.equal((await res.json()).allowed, true) res = await pruefen('kunde-a', svc.newShareToken()) assert.equal(res.status, 403) res = await pruefen('kunde-a', '') assert.equal(res.status, 403) }) test('Neuer Link oder Abschalten macht den alten sofort ungueltig', async () => { const alt = (await svc.renewShareLink('p1')).shareToken assert.equal((await pruefen('kunde-a', alt)).status, 200) const neu = (await svc.renewShareLink('p1')).shareToken assert.equal((await pruefen('kunde-a', alt)).status, 403) assert.equal((await pruefen('kunde-a', neu)).status, 200) await svc.removeShareLink('p1') assert.equal(doc('p1').shareToken, '') assert.equal((await pruefen('kunde-a', neu)).status, 403) })