17ca6399e0
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>
60 lines
2.1 KiB
TypeScript
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);
|
|
});
|
|
});
|