Files
API/test/feedback/events.admin.service.test.ts
T
Paddy 17ca6399e0 Add Feedback domain module: public submission flow, admin CRUD, reporting
New /feedback API domain backed by its own FEEDBACK_DB, mirroring the
Calendar domain's router -> service -> DB pool layering:

- Public endpoints (no auth): eligible-events listing, event config,
  submission with honeypot + rate limiting (in-memory + DB backstop).
- Admin endpoints (session-header auth, reusing Calendar's users/sessions
  via a swappable feedback.auth.ts boundary): events/songs/questions CRUD,
  bulk reorder/assignment, aggregated reporting, CSV export.
- Schema in sql/feedback/001_init.sql (8 tables), applied and verified
  against the real FEEDBACK_DB.
- 64 Jest tests covering validation, auth, rate limiting, CSV escaping,
  and report aggregation (pure functions, no DB needed).

Includes fixes from a security review: path traversal defense doesn't
apply here (that's the frontend proxy, separate repo), but the
rate-limiter cluster does - recordSubmission now counts every processed
request (not just successful ones), the in-memory Map evicts empty
entries instead of growing unbounded, FEEDBACK_IP_SALT is required at
boot instead of silently degrading to unsalted hashing, and submission
answer/rating arrays are capped and de-duplicated to bound insert
amplification.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-05 23:32:22 +02:00

60 lines
2.1 KiB
TypeScript

import {computeDefaultDeadline, slugBase, slugifyName} from '../../src/models/feedback/admin/events.admin.service';
describe('slugifyName', () => {
it('lowercases and hyphenates', () => {
expect(slugifyName('Sommerkonzert 2026')).toBe('sommerkonzert-2026');
});
it('transliterates umlauts', () => {
expect(slugifyName('Frühlingskonzert')).toBe('fruehlingskonzert');
expect(slugifyName('Weihnachtsgrüße')).toBe('weihnachtsgruesse');
});
it('strips punctuation and collapses separators', () => {
expect(slugifyName('Konzert: "Klänge & Farben"!')).toBe('konzert-klaenge-farben');
});
it('trims leading and trailing hyphens', () => {
expect(slugifyName(' -- Herbstkonzert -- ')).toBe('herbstkonzert');
});
});
describe('slugBase', () => {
it('appends the concert year when the name does not already carry it', () => {
expect(slugBase('Sommerkonzert', '2026-08-01')).toBe('sommerkonzert-2026');
});
it('does not double up the year when the name already ends with it', () => {
expect(slugBase('Adventskonzert 2026', '2026-12-06')).toBe('adventskonzert-2026');
});
it('still appends the year when the name contains a different year', () => {
expect(slugBase('Jubiläum 2020', '2026-08-01')).toBe('jubilaeum-2020-2026');
});
});
describe('computeDefaultDeadline', () => {
it('is 14 days after the event date, at 23:59:59', () => {
const deadline = computeDefaultDeadline('2026-08-01');
expect(deadline.getFullYear()).toBe(2026);
expect(deadline.getMonth()).toBe(7); // August = index 7
expect(deadline.getDate()).toBe(15);
expect(deadline.getHours()).toBe(23);
expect(deadline.getMinutes()).toBe(59);
expect(deadline.getSeconds()).toBe(59);
});
it('rolls over the month correctly', () => {
const deadline = computeDefaultDeadline('2026-08-25');
expect(deadline.getMonth()).toBe(8); // September
expect(deadline.getDate()).toBe(8);
});
it('rolls over the year correctly', () => {
const deadline = computeDefaultDeadline('2026-12-25');
expect(deadline.getFullYear()).toBe(2027);
expect(deadline.getMonth()).toBe(0); // January
expect(deadline.getDate()).toBe(8);
});
});