Shop H2: Standalone-Seiten GET /shop/:siteKey und /shop/:siteKey/widerruf mit neutraler Seite, Schutz-Headern und Route-Test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 09:07:48 +00:00
1 parent 583a2d33c2
commit eb616ddbe8
4 files changed
+169

No files matched your search

+81
View File
@@ -0,0 +1,81 @@
import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { useTestEnv } from './support/testEnv.js'
// Task H2: oeffentliche Shop-Seiten /shop/:siteKey und /shop/:siteKey/widerruf
useTestEnv({ appwriteUrl: 'http://127.0.0.1:1', stripeUrl: 'http://127.0.0.1:1' })
const { startTestApp } = await import('./support/testApp.js')
const { shopSeiteRouter, resetShopSeiteForTests } = await import('../server/routes/shopSeite.js')
resetShopSeiteForTests()
const app = await startTestApp({ '/': shopSeiteRouter() })
after(() => app.close())
const KEY = 'abcdefghijklmnop'.replace(/[ilo]/g, 'a') // nur a-z und 2-7 wie siteKeys
const get = async (path) => {
const r = await fetch(app.url + path)
return { status: r.status, headers: r.headers, text: await r.text() }
}
const lies = (pfad) => readFileSync(new URL(`../${pfad}`, import.meta.url), 'utf8')
test('gueltiger Schluessel: 200 HTML mit Schluessel im Container und den Schutz-Headern', async () => {
const r = await get(`/shop/${KEY}`)
assert.equal(r.status, 200)
assert.match(r.headers.get('content-type'), /^text\/html; charset=utf-8/)
assert.match(r.text, new RegExp(`<div data-webklar-shop="${KEY}" data-ansicht="" data-api="">`))
assert.doesNotMatch(r.text, /__SITEKEY__|__ANSICHT__/)
assert.match(r.text, /<script src="\/embed\/shop\.js" defer><\/script>/)
assert.equal(r.headers.get('cache-control'), 'no-store')
assert.equal(r.headers.get('x-content-type-options'), 'nosniff')
assert.equal(r.headers.get('referrer-policy'), 'strict-origin-when-cross-origin')
assert.equal(r.headers.get('x-robots-tag'), 'noindex')
const csp = r.headers.get('content-security-policy')
for (const d of ["default-src 'self'", "script-src 'self'", "connect-src 'self'", "img-src 'self' data:", "frame-ancestors 'none'", "base-uri 'none'", "form-action 'self'"]) {
assert.ok(csp.includes(d), d)
}
assert.ok(!/script-src[^;]*unsafe/.test(csp), 'keine unsicheren Skripte')
})
test('/widerruf setzt data-ansicht="widerruf"', async () => {
const r = await get(`/shop/${KEY}/widerruf`)
assert.equal(r.status, 200)
assert.match(r.text, new RegExp(`data-webklar-shop="${KEY}" data-ansicht="widerruf"`))
assert.equal(r.headers.get('x-robots-tag'), 'noindex')
})
test('jeder syntaktisch gueltige Schluessel liefert dieselbe Seite (kein Orakel)', async () => {
const a = await get(`/shop/${KEY}`)
const b = await get('/shop/2222222222222222')
assert.equal(b.status, 200)
assert.equal(a.text.replaceAll(KEY, 'X'), b.text.replaceAll('2222222222222222', 'X'))
})
test('ungueltige Schluessel und fremde Pfade: 404', async () => {
for (const p of ['/shop/xyz', `/shop/${KEY.toUpperCase()}`, `/shop/${KEY}a`, '/shop/abcdefghijklmn01', '/shop/xyz/abc', `/shop/${KEY}/agb`, `/shop/${KEY}/widerruf/x`, '/shop/%3Cscript%3E', '/shop', '/shop/']) {
const r = await get(p)
assert.equal(r.status, 404, p)
assert.doesNotMatch(r.text, /data-webklar-shop/, p)
}
})
test('nur GET', async () => {
const r = await fetch(`${app.url}/shop/${KEY}`, { method: 'POST' })
assert.equal(r.status, 404)
})
test('shop-seite.html: kein Inline-Skript, nur das Widget; in server/index.js vor express.static gemountet', () => {
const html = lies('public/shop-seite.html')
const skripte = [...html.matchAll(/<script\b([^>]*)>([\s\S]*?)<\/script>/gi)]
assert.equal(skripte.length, 1)
assert.match(skripte[0][1], /\bsrc="\/embed\/shop\.js"/)
assert.equal(skripte[0][2].trim(), '')
assert.doesNotMatch(html, /\son[a-z]+\s*=/i, 'keine Inline-Handler')
assert.doesNotMatch(html, /javascript:/i)
assert.match(html, /<html lang="de">/)
assert.match(html, /<title>Shop<\/title>/)
assert.match(html, /Der Shop wird geladen …/)
const index = lies('server/index.js')
const mount = index.indexOf('app.use(shopSeiteRouter())')
assert.ok(mount > 0 && mount < index.indexOf('app.use(express.static(publicDir))'))
})