183 lines
9.5 KiB
JavaScript
183 lines
9.5 KiB
JavaScript
import { test, after } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import { mkdtempSync } from 'node:fs'
|
|
import sharp from 'sharp'
|
|
import { startAppwriteMock } from './support/appwriteMock.js'
|
|
import { startFakeStripe } from './support/fakeStripe.js'
|
|
import { useTestEnv } from './support/testEnv.js'
|
|
import { schemaKeys } from './support/schema.js'
|
|
|
|
process.env.NEWS_UPLOAD_DIR = mkdtempSync(path.join(os.tmpdir(), 'news-routes-'))
|
|
const aw = await startAppwriteMock({
|
|
schema: { portalFeatures: schemaKeys('portalFeatures'), newsPosts: schemaKeys('newsPosts') },
|
|
})
|
|
const st = await startFakeStripe()
|
|
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
|
|
const { startTestApp, get, post, sessionCookie } = await import('./support/testApp.js')
|
|
const { customerCookie, seedFixtures } = await import('./support/fixtures.js')
|
|
const { default: newsRoutes } = await import('../server/routes/news.js')
|
|
const { default: publicNewsRoutes } = await import('../server/routes/publicNews.js')
|
|
|
|
// Waechter wie in server/index.js: alte Ansichten ohne Kennung (frueheres Admin-Panel) gelten als beendet
|
|
const { kundenansichtGuard } = await import('../server/middleware/kundenansichtGuard.js')
|
|
const app = await startTestApp({ '/': kundenansichtGuard, '/api/public/news': publicNewsRoutes, '/api/news': newsRoutes })
|
|
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
|
seedFixtures(aw)
|
|
|
|
const KEY = 'abcdefghijklmnop'
|
|
const KEY2 = 'bcdefghijklmnopq'
|
|
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei', liveDomain: 'baeckerei.de' })
|
|
aw.seed('websiteProjects', { $id: 'p2', customerId: 'c1', projectName: 'Café' })
|
|
aw.seed('websiteProjects', { $id: 'p3', customerId: 'c2', projectName: 'Rate' })
|
|
const feature = (projectId, customerId, state, siteKey) =>
|
|
aw.seed('portalFeatures', { $id: `f_${projectId}_news`, customerId, projectId, featureKey: 'news', state, enabled: state === 'active', siteKey })
|
|
feature('p1', 'c1', 'active', KEY)
|
|
feature('p2', 'c1', 'setup', KEY2)
|
|
feature('p3', 'c2', 'active', 'cdefghijklmnopqr')
|
|
|
|
const cookie = customerCookie('c1')
|
|
const call = (method, url, { body, cookie: c = cookie, headers = {} } = {}) =>
|
|
fetch(app.url + url, {
|
|
method,
|
|
headers: { ...(c ? { cookie: c } : {}), ...(body !== undefined && !Buffer.isBuffer(body) ? { 'content-type': 'application/json' } : {}), ...headers },
|
|
body: body === undefined ? undefined : Buffer.isBuffer(body) ? body : JSON.stringify(body),
|
|
})
|
|
const note = { projectId: 'p1', type: 'note', title: 'Hallo', text: 'Wir haben offen', paper: 'sun', duration: '1m' }
|
|
const png = () => sharp({ create: { width: 40, height: 30, channels: 3, background: '#c33' } }).png().toBuffer()
|
|
const upload = (projectId, c = cookie) =>
|
|
png().then((buf) => call('POST', `/api/news/image?projectId=${projectId}`, { body: buf, cookie: c, headers: { 'content-type': 'application/octet-stream' } }))
|
|
|
|
test('nicht_aktiv_409', async () => {
|
|
const res = await post('/api/news', { ...note, projectId: 'p2' }, { cookie })
|
|
assert.equal(res.status, 409)
|
|
assert.equal(res.body.error, 'News ist für diese Website nicht aktiv.')
|
|
})
|
|
|
|
test('fremdes_projekt_403_und_alte_Ansicht_401', async () => {
|
|
assert.equal((await post('/api/news', { ...note, projectId: 'p3' }, { cookie })).status, 403)
|
|
const alt = sessionCookie({ customerId: 'c1', appwriteUserId: 'u1', viewAs: true })
|
|
const res = await post('/api/news', note, { cookie: alt })
|
|
assert.equal(res.status, 401)
|
|
assert.equal(res.body.code, 'view_expired')
|
|
})
|
|
|
|
test('kunde_crud', async () => {
|
|
const created = await post('/api/news', note, { cookie })
|
|
assert.equal(created.status, 201)
|
|
const id = created.body.$id
|
|
const list = await get('/api/news?projectId=p1', { cookie })
|
|
assert.equal(list.hanging.length, 1)
|
|
assert.equal(list.liveUrl, 'https://baeckerei.de')
|
|
const patched = await call('PATCH', `/api/news/${id}`, { body: { projectId: 'p1', action: 'takeDown' } })
|
|
assert.equal(patched.status, 200)
|
|
const after = await get('/api/news?projectId=p1', { cookie })
|
|
assert.deepEqual([after.hanging.length, after.expired.length], [0, 1])
|
|
assert.equal((await call('DELETE', `/api/news/${id}?projectId=p1`)).status, 204)
|
|
assert.equal((await get('/api/news?projectId=p1', { cookie })).expired.length, 0)
|
|
})
|
|
|
|
test('upload_und_vorschau', async () => {
|
|
const res = await upload('p1')
|
|
assert.equal(res.status, 201)
|
|
const { imageId } = await res.json()
|
|
const ok = await call('GET', `/api/news/image/p1/${imageId}-s.webp`)
|
|
assert.equal(ok.status, 200)
|
|
assert.equal(ok.headers.get('content-type'), 'image/webp')
|
|
await ok.arrayBuffer()
|
|
const other = await call('GET', `/api/news/image/p1/${imageId}-s.webp`, { cookie: customerCookie('c2') })
|
|
assert.equal(other.status, 403)
|
|
assert.equal((await call('GET', '/api/news/image/p1/..%2Fx-l.webp')).status, 404)
|
|
})
|
|
|
|
test('oeffentlich_cors_cache', async () => {
|
|
const { imageId } = await (await upload('p1')).json()
|
|
const created = await post('/api/news', { ...note, type: 'combo', imageId }, { cookie })
|
|
assert.equal(created.status, 201)
|
|
const res = await call('GET', `/api/public/news/${KEY}`, { cookie: null })
|
|
assert.equal(res.status, 200)
|
|
assert.equal(res.headers.get('access-control-allow-origin'), '*')
|
|
assert.equal(res.headers.get('cache-control'), 'public, max-age=60')
|
|
const text = await res.text()
|
|
assert.ok(!text.includes('"c1"') && !text.includes('"p1"') && !text.includes('customerId') && !text.includes('projectId'))
|
|
const body = JSON.parse(text)
|
|
assert.equal(body.posts.length, 1)
|
|
assert.equal(body.posts[0].image.src, `/api/public/news/${KEY}/img/${created.body.$id}.webp?v=${Date.parse(created.body.updatedAt)}`)
|
|
assert.equal(body.posts[0].image.srcSmall, `/api/public/news/${KEY}/img/${created.body.$id}-s.webp?v=${Date.parse(created.body.updatedAt)}`)
|
|
const etag = res.headers.get('etag')
|
|
assert.match(etag, /^W\//)
|
|
const again = await call('GET', `/api/public/news/${KEY}`, { cookie: null, headers: { 'if-none-match': etag } })
|
|
assert.equal(again.status, 304)
|
|
const img = await call('GET', body.posts[0].image.src, { cookie: null })
|
|
assert.equal(img.status, 200)
|
|
assert.equal(img.headers.get('cache-control'), 'public, max-age=86400')
|
|
assert.equal(img.headers.get('x-content-type-options'), 'nosniff')
|
|
assert.equal(img.headers.get('access-control-allow-origin'), '*')
|
|
await img.arrayBuffer()
|
|
const pre = await call('OPTIONS', `/api/public/news/${KEY}`, { cookie: null })
|
|
assert.equal(pre.status, 204)
|
|
})
|
|
|
|
test('oeffentlich_unbekannt_leer', async () => {
|
|
for (const key of ['zzzzzzzzzzzzzzzz', 'kaputt', KEY2]) {
|
|
const res = await call('GET', `/api/public/news/${key}`, { cookie: null })
|
|
assert.equal(res.status, 200)
|
|
assert.deepEqual(await res.json(), { posts: [], hasMore: false })
|
|
}
|
|
})
|
|
|
|
test('bild_nur_wenn_haengend', async () => {
|
|
const { imageId } = await (await upload('p1')).json()
|
|
const created = await post('/api/news', { ...note, type: 'photo', imageId }, { cookie })
|
|
const url = `/api/public/news/${KEY}/img/${created.body.$id}.webp`
|
|
assert.equal((await call('GET', url, { cookie: null })).status, 200)
|
|
await call('PATCH', `/api/news/${created.body.$id}`, { body: { projectId: 'p1', action: 'takeDown' } })
|
|
assert.equal((await call('GET', url, { cookie: null })).status, 404)
|
|
assert.equal((await call('GET', `/api/public/news/${KEY}/img/nichtda.webp`, { cookie: null })).status, 404)
|
|
})
|
|
|
|
test('rate_limit_schreiben', async () => {
|
|
const c2 = customerCookie('c2')
|
|
const statuses = []
|
|
for (let i = 0; i < 31; i++) statuses.push((await post('/api/news', { ...note, projectId: 'p3' }, { cookie: c2 })).status)
|
|
assert.ok(statuses.slice(0, 30).every((s) => s === 201), statuses.join())
|
|
assert.equal(statuses[30], 429)
|
|
})
|
|
|
|
test('cache_buster_aendert_sich_beim_bildtausch', async () => {
|
|
const first = await (await upload('p1')).json()
|
|
const created = await post('/api/news', { ...note, type: 'photo', imageId: first.imageId }, { cookie })
|
|
const src = async () => (await (await call('GET', `/api/public/news/${KEY}`, { cookie: null })).json()).posts.find((p) => p.id === created.body.$id).image
|
|
const before = await src()
|
|
assert.match(before.src, /\.webp\?v=\d+$/)
|
|
assert.match(before.srcSmall, /-s\.webp\?v=\d+$/)
|
|
await new Promise((r) => setTimeout(r, 15))
|
|
const second = await (await upload('p1')).json()
|
|
const res = await call('PATCH', `/api/news/${created.body.$id}`, { body: { ...note, type: 'photo', imageId: second.imageId } })
|
|
assert.equal(res.status, 200)
|
|
const now = await src()
|
|
assert.notEqual(now.src, before.src)
|
|
assert.equal((await call('GET', now.src, { cookie: null })).status, 200)
|
|
})
|
|
|
|
test('tippfehler_zaehlt_nicht_gegen_schreiblimit', async () => {
|
|
aw.seed('websiteProjects', { $id: 'p4', customerId: 'c3', projectName: 'Tippfehler' })
|
|
feature('p4', 'c3', 'active', 'defghijklmnopqrs')
|
|
const c = customerCookie('c3')
|
|
for (let i = 0; i < 35; i++) assert.equal((await post('/api/news', { ...note, projectId: 'gibtsnicht' }, { cookie: c })).status, 403)
|
|
assert.equal((await post('/api/news', { ...note, projectId: 'p4' }, { cookie: c })).status, 201)
|
|
})
|
|
|
|
test('head_erlaubt', async () => {
|
|
assert.equal((await call('HEAD', `/api/public/news/${KEY}`, { cookie: null })).status, 200)
|
|
})
|
|
|
|
test('oeffentlich_rate_limit_429', async () => {
|
|
const ip = { 'x-forwarded-for': '203.0.113.77' }
|
|
const statuses = []
|
|
for (let i = 0; i < 121; i++) statuses.push((await call('GET', `/api/public/news/${KEY2}`, { cookie: null, headers: ip })).status)
|
|
assert.ok(statuses.slice(0, 120).every((s) => s === 200))
|
|
assert.equal(statuses[120], 429)
|
|
})
|