feat: DB backend and API implemementation

This commit is contained in:
Robert Wartenberg committed 2026-08-28 10:13:55 +02:00
1 parent 39b891cd63
commit a78e84515a
14 files changed
+955 -652

No files matched your search

@@ -0,0 +1,24 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
test('liefert eine externe, idempotente Anfangsmigration für Termine und Anmeldungen', async () => {
const migration = await readFile(new URL('../database/migrations/001_booking_schema.sql', import.meta.url), 'utf8');
assert.match(migration, /CREATE TABLE IF NOT EXISTS `sufw_booking_events`/);
assert.match(migration, /CREATE TABLE IF NOT EXISTS `sufw_booking_registrations`/);
assert.match(migration, /FOREIGN KEY \(`event_id`\)/);
assert.match(migration, /CHECK \(`capacity` BETWEEN 1 AND 100\)/);
});
test('hält die statische Vorschaukopie der Buchungsassets identisch', async () => {
const [componentScript, previewScript, componentStyle, previewStyle] = await Promise.all([
readFile(new URL('../public/booking/booking.js', import.meta.url), 'utf8'),
readFile(new URL('../public/perspektivwerk/js/booking.js', import.meta.url), 'utf8'),
readFile(new URL('../public/booking/booking.css', import.meta.url), 'utf8'),
readFile(new URL('../public/perspektivwerk/js/booking.css', import.meta.url), 'utf8'),
]);
assert.equal(previewScript, componentScript);
assert.equal(previewStyle, componentStyle);
});
+28 -49
View File
@@ -1,73 +1,53 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { bookingConfirmationMessage, canSubmitBookingForm, formatDateKey, getSlotsForDate, validateBookingField, validateBookingForm } from '../public/booking/booking.js';
import { bookingConfirmationMessage, canSubmitBookingForm, formatDateKey, formatEventDuration, getSlotsForDate, requiresIntroductionDetails, validateBookingForm } from '../public/booking/booking.js';
test('liefert für einen Beispieltag verfügbare und belegte Slots', () => {
const slots = getSlotsForDate('perspektivwerk', '2026-08-25');
const events = [
{ id: 'one', project: 'perspektivwerk', startsAt: '2026-09-14 08:00:00', endsAt: '2026-09-18 16:00:00', status: 'available' },
{ id: 'two', project: 'perspektivwerk', startsAt: '2026-09-15 09:00:00', endsAt: '2026-09-15 13:00:00', status: 'full' },
];
assert.equal(slots.length, 3);
assert.equal(slots.filter((slot) => slot.status === 'available').length, 2);
assert.equal(slots.filter((slot) => slot.status === 'booked').length, 1);
test('liefert auch mehrtägige Termine für jeden betroffenen Kalendertag', () => {
const slots = getSlotsForDate('perspektivwerk', '2026-09-15', events);
assert.equal(slots.length, 2);
assert.equal(slots.filter((slot) => slot.status === 'available').length, 1);
});
test('liefert für einen Tag ohne Termin keine Slots', () => {
assert.deepEqual(getSlotsForDate('produktionsschule', '2026-08-20'), []);
assert.deepEqual(getSlotsForDate('produktionsschule', '2026-09-15', events), []);
});
test('verschiebt Kalendertage in der Sommerzeit nicht auf den Vortag', () => {
process.env.TZ = 'Europe/Berlin';
assert.equal(formatDateKey(new Date(2026, 7, 24)), '2026-08-24');
});
test('liefert Termine im aktuellen und folgenden Monat', () => {
assert.ok(getSlotsForDate('perspektivwerk', '2026-08-24').length >= 2);
assert.ok(getSlotsForDate('perspektivwerk', '2026-08-28').length >= 2);
assert.ok(getSlotsForDate('perspektivwerk', '2026-09-02').length >= 2);
assert.ok(getSlotsForDate('perspektivwerk', '2026-09-15').length >= 2);
assert.ok(getSlotsForDate('perspektivwerk', '2026-09-29').length >= 2);
test('validiert die Pflichtangaben der Buchungsmaske', () => {
assert.deepEqual(validateBookingForm({ name: '', email: 'ungültig', privacyAccepted: false }), {
name: 'Bitte geben Sie Ihren Namen ein.', email: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.', phone: 'Bitte geben Sie eine gültige Telefonnummer ein.', privacy: 'Bitte bestätigen Sie die Datenschutzhinweise.'
});
});
test('validiert die Pflichtangaben der Buchungsmaske', () => {
assert.deepEqual(
validateBookingForm({ name: '', email: 'ungültig', privacyAccepted: false }),
{
name: 'Bitte geben Sie Ihren Namen ein.',
email: 'Bitte geben Sie eine gültige E-Mail-Adresse ein.',
privacy: 'Bitte bestätigen Sie die Datenschutzhinweise.'
}
);
test('verlangt im SOLL-Stand eine Telefonnummer', () => {
assert.equal(validateBookingForm({ name: 'Max Mustermann', email: 'max@example.org', privacyAccepted: true }).phone, 'Bitte geben Sie eine gültige Telefonnummer ein.');
});
test('akzeptiert die vollständigen Pflichtangaben der Buchungsmaske', () => {
assert.deepEqual(
validateBookingForm({ name: 'Max Mustermann', email: 'max@example.org', privacyAccepted: true }),
{}
);
assert.deepEqual(validateBookingForm({ name: 'Max Mustermann', email: 'max@example.org', phone: '+49 351 123456', privacyAccepted: true }), {});
});
test('lehnt ein eingegebenes Alter außerhalb von 12 bis 112 ab', () => {
assert.equal(
validateBookingForm({ name: 'Max Mustermann', email: 'max@example.org', age: '11', privacyAccepted: true }).age,
'Bitte geben Sie ein Alter zwischen 12 und 112 Jahren ein.'
);
assert.equal(
validateBookingForm({ name: 'Max Mustermann', email: 'max@example.org', age: '113', privacyAccepted: true }).age,
'Bitte geben Sie ein Alter zwischen 12 und 112 Jahren ein.'
);
test('setzt die Zusatzangaben für Probearbeiten durch', () => {
assert.equal(requiresIntroductionDetails({ project: 'produktionsschule', introductionRequired: true }), true);
assert.equal(requiresIntroductionDetails({ project: 'perspektivwerk', introductionRequired: true }), false);
assert.deepEqual(validateBookingForm({ name: 'Max Mustermann', email: 'max@example.org', phone: '+49 351 123456', privacyAccepted: true, event: { project: 'produktionsschule', introductionRequired: true } }), {
introductionAttended: 'Bitte bestätigen Sie die Teilnahme an einer Einführungsveranstaltung.', introductionDate: 'Bitte geben Sie das Datum der Einführungsveranstaltung ein.'
});
});
test('akzeptiert ein leeres oder gültiges optionales Alter', () => {
assert.deepEqual(validateBookingForm({ name: 'Max Mustermann', email: 'max@example.org', age: '', privacyAccepted: true }), {});
assert.deepEqual(validateBookingForm({ name: 'Max Mustermann', email: 'max@example.org', age: '12', privacyAccepted: true }), {});
assert.deepEqual(validateBookingForm({ name: 'Max Mustermann', email: 'max@example.org', age: '112', privacyAccepted: true }), {});
});
test('validiert optionale Telefonnummern direkt am Feld', () => {
assert.equal(validateBookingField('phone', 'abc'), 'Bitte geben Sie eine gültige Telefonnummer ein.');
assert.equal(validateBookingField('phone', '+49 351 123456'), '');
assert.equal(validateBookingField('phone', ''), '');
test('formatiert kurze und mehrtägige Termindauern', () => {
assert.equal(formatEventDuration({ startsAt: '2026-09-15 09:00:00', endsAt: '2026-09-15 13:00:00' }), '4 Stunden');
assert.equal(formatEventDuration({ startsAt: '2026-09-21 08:00:00', endsAt: '2026-09-25 16:00:00' }), '5 Tage');
});
test('gibt den Absenden-Button erst nach DSGVO-Einwilligung frei', () => {
@@ -75,9 +55,8 @@ test('gibt den Absenden-Button erst nach DSGVO-Einwilligung frei', () => {
assert.equal(canSubmitBookingForm(true), true);
});
test('zeigt nach dem Absenden eine Bestätigung ohne Mockup-Hinweis', () => {
test('zeigt nach dem Absenden eine verbindliche Bestätigung ohne Mockup-Hinweis', () => {
const message = bookingConfirmationMessage('Erstgespräch');
assert.match(message, /erfolgreich übermittelt/);
assert.match(message, /verbindlich gespeichert/);
assert.doesNotMatch(message, /Vorschau|Mockup/i);
});