c2ddb11c4c
Three fixes from the earlier review, plus cleanup: - submissions.service.ts: a newsletter opt-in present but failing validation (e.g. malformed email) was silently dropped with no signal to the client - the rest of the submission saved, but the visitor had no way to know their newsletter signup didn't go through. Added newsletterDropped to the submit response so the frontend can tell them. - reports.admin.service.ts: the newsletter summary tracked total/sent/pending/failed but silently omitted SKIPPED (stub-mode) signups from any bucket - every current signup showed total>0 with every bucket reading 0, indistinguishable from "we don't know what happened". Added a skipped count. - Consolidated two things duplicated across the module: sendServerError (reimplemented ~11 times, three of those as identical local copies of the same function) into feedback.errors.ts, and formatDatetime/ toMysqlDatetime (the same local-time formatting logic under two names, in csv.service.ts and events.admin.service.ts respectively) into feedback.dates.ts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
59 lines
2.0 KiB
TypeScript
59 lines
2.0 KiB
TypeScript
import {escapeCsvField} from '../../src/models/feedback/admin/csv.service';
|
|
import {formatDatetime} from '../../src/models/feedback/feedback.dates';
|
|
|
|
describe('escapeCsvField', () => {
|
|
it('passes plain text through unchanged', () => {
|
|
expect(escapeCsvField('Abendlied')).toBe('Abendlied');
|
|
});
|
|
|
|
it('converts null/undefined to an empty string', () => {
|
|
expect(escapeCsvField(null)).toBe('');
|
|
expect(escapeCsvField(undefined)).toBe('');
|
|
});
|
|
|
|
it('converts numbers to strings', () => {
|
|
expect(escapeCsvField(5)).toBe('5');
|
|
});
|
|
|
|
it('quotes and doubles internal quotes (RFC 4180)', () => {
|
|
expect(escapeCsvField('Sie sagte "Danke"')).toBe('"Sie sagte ""Danke"""');
|
|
});
|
|
|
|
it('quotes a field containing the ; separator', () => {
|
|
expect(escapeCsvField('Rheinberger; Bach')).toBe('"Rheinberger; Bach"');
|
|
});
|
|
|
|
it('strips embedded newlines instead of breaking the row', () => {
|
|
expect(escapeCsvField('Zeile 1\r\nZeile 2')).toBe('Zeile 1 Zeile 2');
|
|
expect(escapeCsvField('Zeile 1\nZeile 2')).toBe('Zeile 1 Zeile 2');
|
|
});
|
|
|
|
it('prefixes formula-injection characters with an apostrophe', () => {
|
|
expect(escapeCsvField('=1+1')).toBe("'=1+1");
|
|
expect(escapeCsvField('+SUM(A1)')).toBe("'+SUM(A1)");
|
|
expect(escapeCsvField('-2')).toBe("'-2");
|
|
expect(escapeCsvField('@example')).toBe("'@example");
|
|
});
|
|
|
|
it('does not treat a mid-string = as formula injection', () => {
|
|
expect(escapeCsvField('x = y')).toBe('x = y');
|
|
});
|
|
});
|
|
|
|
describe('formatDatetime', () => {
|
|
it('formats a Date as YYYY-MM-DD HH:mm:ss, not the verbose Date.toString()', () => {
|
|
const d = new Date(2026, 7, 2, 21, 59, 21); // month is 0-indexed: August
|
|
expect(formatDatetime(d)).toBe('2026-08-02 21:59:21');
|
|
expect(formatDatetime(d)).not.toContain('GMT');
|
|
});
|
|
|
|
it('pads single-digit components', () => {
|
|
const d = new Date(2026, 0, 5, 3, 4, 5);
|
|
expect(formatDatetime(d)).toBe('2026-01-05 03:04:05');
|
|
});
|
|
|
|
it('returns an empty string for null', () => {
|
|
expect(formatDatetime(null)).toBe('');
|
|
});
|
|
});
|