Push-Zeitmail: Bestätigungsseite /zeit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1a1005d099
commit
a28ae21389
3 files changed
+381
No files matched your search
@@ -0,0 +1,161 @@
|
||||
import { test, before, after, beforeEach } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import express from 'express'
|
||||
import { startFakeAppwrite } from './helpers/fakeAppwrite.js'
|
||||
|
||||
let fake
|
||||
let server
|
||||
let baseUrl
|
||||
let createPushTimeToken
|
||||
let resetPushTimeRateLimitForTests
|
||||
|
||||
const gitSheet = (id, extra = {}) => ({
|
||||
$id: id, wsid: '100300', woid: '100118', workorderId: 't1', serviceType: 'GIT',
|
||||
employeeName: 'knso', endDate: '30.09.2026', endTime: '1646', startDate: '30.09.2026', startTime: '',
|
||||
totalTime: 0, isComment: true,
|
||||
details: 'Git Push auf `WEBklar/amin-friseur` (Branch: main)\nCommit: abc1234\n\n- [abc1234](https://git/x): Startseite neu (knso)',
|
||||
...extra,
|
||||
})
|
||||
|
||||
before(async () => {
|
||||
fake = await startFakeAppwrite({
|
||||
seed: {
|
||||
workorders: [{ $id: 't1', woid: '100118', customerName: 'Amin Friseur' }],
|
||||
worksheets: [
|
||||
gitSheet('open1'),
|
||||
gitSheet('open2'),
|
||||
gitSheet('open3'),
|
||||
gitSheet('done1', { startTime: '1500', totalTime: 45, isComment: false }),
|
||||
gitSheet('evil', { details: 'Git Push auf `X` (Branch: main)\n\n- [abc1234](u): <b>fett</b>' }),
|
||||
{ $id: 'remote1', serviceType: 'Remote', startTime: '', totalTime: 0 },
|
||||
],
|
||||
},
|
||||
})
|
||||
Object.assign(process.env, {
|
||||
APPWRITE_ENDPOINT: fake.endpoint,
|
||||
APPWRITE_API_KEY: 'test-key',
|
||||
PUSH_TIME_SECRET: 'r'.repeat(40),
|
||||
})
|
||||
const routes = await import('../server/routes/pushTime.js')
|
||||
resetPushTimeRateLimitForTests = routes.resetPushTimeRateLimitForTests
|
||||
;({ createPushTimeToken } = await import('../server/services/pushTimeToken.js'))
|
||||
|
||||
const app = express()
|
||||
app.use('/zeit', express.urlencoded({ extended: false, limit: '4kb' }), routes.default)
|
||||
server = await new Promise((resolve) => {
|
||||
const s = app.listen(0, '127.0.0.1', () => resolve(s))
|
||||
})
|
||||
baseUrl = `http://127.0.0.1:${server.address().port}`
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
if (server) await new Promise((resolve) => server.close(resolve))
|
||||
await fake.close()
|
||||
})
|
||||
|
||||
beforeEach(() => resetPushTimeRateLimitForTests?.())
|
||||
|
||||
const token = (worksheetId, now) => createPushTimeToken({ worksheetId, employeeId: 'k' }, now)
|
||||
|
||||
async function get(path) {
|
||||
const res = await fetch(`${baseUrl}${path}`)
|
||||
return { status: res.status, html: await res.text(), headers: res.headers }
|
||||
}
|
||||
|
||||
async function post(path, form) {
|
||||
const res = await fetch(`${baseUrl}${path}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(form).toString(),
|
||||
})
|
||||
return { status: res.status, html: await res.text(), headers: res.headers }
|
||||
}
|
||||
|
||||
test('GET speichert nie', async () => {
|
||||
const t = token('open1')
|
||||
for (const m of ['15', '30', '45', '60', '90', '120', '180', '']) {
|
||||
const r = await get(`/zeit/${t}${m ? `?min=${m}` : ''}`)
|
||||
assert.equal(r.status, 200)
|
||||
assert.match(r.html, /#100118/)
|
||||
assert.match(r.html, /Amin Friseur/)
|
||||
assert.match(r.html, /WEBklar\/amin-friseur/)
|
||||
assert.match(r.html, /Startseite neu/)
|
||||
}
|
||||
const ws = fake.get('worksheets', 'open1')
|
||||
assert.equal(ws.totalTime, 0)
|
||||
assert.equal(ws.startTime, '')
|
||||
})
|
||||
|
||||
test('GET mit min=60 füllt das Feld vor, Unsinn nicht', async () => {
|
||||
const t = token('open1')
|
||||
assert.match((await get(`/zeit/${t}?min=60`)).html, /name="minutes"[^>]*value="60"/)
|
||||
assert.match((await get(`/zeit/${t}?min=abc`)).html, /name="minutes"[^>]*value=""/)
|
||||
})
|
||||
|
||||
test('POST speichert', async () => {
|
||||
const r = await post(`/zeit/${token('open2')}`, { minutes: '45' })
|
||||
assert.equal(r.status, 200)
|
||||
assert.match(r.html, /Eingetragen: 45 Min auf #100118/)
|
||||
const ws = fake.get('worksheets', 'open2')
|
||||
assert.equal(ws.totalTime, 45)
|
||||
assert.equal(ws.isComment, false)
|
||||
assert.equal(ws.startTime, '1601')
|
||||
})
|
||||
|
||||
test('POST mit ungültigen Minuten', async () => {
|
||||
for (const minutes of ['0', '721', '1.5', '']) {
|
||||
const r = await post(`/zeit/${token('open3')}`, { minutes })
|
||||
assert.equal(r.status, 400, minutes)
|
||||
assert.match(r.html, /Bitte ganze Minuten zwischen 1 und 720 eingeben\./)
|
||||
assert.match(r.html, /name="minutes"/)
|
||||
}
|
||||
assert.equal(fake.get('worksheets', 'open3').totalTime, 0)
|
||||
})
|
||||
|
||||
test('schon eingetragen: nichts wird überschrieben', async () => {
|
||||
const t = token('done1')
|
||||
const g = await get(`/zeit/${t}?min=30`)
|
||||
assert.equal(g.status, 200)
|
||||
assert.match(g.html, /Schon eingetragen: 45 Min/)
|
||||
assert.doesNotMatch(g.html, /name="minutes"/)
|
||||
const p = await post(`/zeit/${t}`, { minutes: '30' })
|
||||
assert.match(p.html, /Schon eingetragen: 45 Min/)
|
||||
assert.equal(fake.get('worksheets', 'done1').totalTime, 45)
|
||||
})
|
||||
|
||||
test('Token-Fehler und fehlende Blätter', async () => {
|
||||
assert.equal((await get('/zeit/muell')).status, 400)
|
||||
const old = token('open1', Date.now() - 15 * 86400 * 1000)
|
||||
const expired = await get(`/zeit/${old}`)
|
||||
assert.equal(expired.status, 410)
|
||||
assert.match(expired.html, /Link abgelaufen/)
|
||||
assert.equal((await post(`/zeit/${old}`, { minutes: '30' })).status, 410)
|
||||
const missing = await get(`/zeit/${token('gibtsnicht')}`)
|
||||
assert.equal(missing.status, 404)
|
||||
assert.match(missing.html, /Nicht mehr vorhanden/)
|
||||
assert.equal((await get(`/zeit/${token('remote1')}`)).status, 400)
|
||||
assert.equal((await post(`/zeit/${token('remote1')}`, { minutes: '30' })).status, 400)
|
||||
})
|
||||
|
||||
test('Escaping', async () => {
|
||||
const r = await get(`/zeit/${token('evil')}`)
|
||||
assert.match(r.html, /<b>fett<\/b>/)
|
||||
assert.doesNotMatch(r.html, /<b>fett/)
|
||||
})
|
||||
|
||||
test('Sicherheits-Header und Ticket-Link', async () => {
|
||||
const r = await get(`/zeit/${token('open1')}`)
|
||||
assert.equal(r.headers.get('cache-control'), 'no-store')
|
||||
assert.equal(r.headers.get('x-robots-tag'), 'noindex')
|
||||
assert.equal(r.headers.get('referrer-policy'), 'no-referrer')
|
||||
assert.match(r.headers.get('content-type'), /text\/html; charset=utf-8/)
|
||||
assert.match(r.html, /https:\/\/ticket\.webklar\.com\/tickets\?woid=100118/)
|
||||
})
|
||||
|
||||
test('Rate-Limit: 31. Anfrage → 429', async () => {
|
||||
const t = token('open1')
|
||||
for (let i = 0; i < 30; i += 1) assert.equal((await get(`/zeit/${t}`)).status, 200)
|
||||
const r = await get(`/zeit/${t}`)
|
||||
assert.equal(r.status, 429)
|
||||
assert.match(r.html, /Zu viele Anfragen/)
|
||||
})
|
||||
Reference in new issue
Block a user