fix(event-types): harden setup window + catalog validation (codex review)
Three review rounds on PR #801; fixes in response: - isValidEventType: live catalog is authoritative when it has rows — a deleted or deactivated slug no longer validates via the legacy fallback (fallback now only serves an empty-catalog install) - deleteEventType: refuse deleting the last (and last ACTIVE) type; updateEventType: refuse deactivating the last active type (unknown slugs are rejected since the validator change, so an empty active catalog would brick event creation) - setup window fails closed: only an explicit stored `false` opens it (a portable-backup restore can leave the key absent) and a normal admin login durably closes it (abandoned-wizard case) - reserved bootstrap keys (setup_wizard_completed, setup_token) are stripped from ALL generic settings upserts (/general, /security, /analytics, /seo) so the marker is genuinely one-way - wizard step: deletes ordered so the catalog can never end up empty, and a genuinely failed system-type deletion reloads the list and stays on the step instead of advancing past the only window in which it can be retried - CreateEventPage: snap the hardcoded initial 'wedding' selection to the first active type when the catalog no longer contains it - v1 API: new GET /event-types (read scope) so token clients can discover valid slugs; OpenAPI enum replaced with the live-catalog description
This commit is contained in:
@@ -75,6 +75,11 @@ describe('event type deletion during the setup window (#800)', () => {
|
||||
|
||||
expect(await db('event_types').where({ slug_prefix: 'wedding' }).first()).toBeFalsy();
|
||||
expect(await db('email_templates').where({ template_key: 'event_reminder_wedding' }).first()).toBeFalsy();
|
||||
|
||||
// The deleted slug must NOT stay creatable through the legacy fallback —
|
||||
// the live catalog is authoritative while it has rows.
|
||||
expect(await eventTypeService.isValidEventType('wedding')).toBe(false);
|
||||
expect(await eventTypeService.isValidEventType('birthday')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not resurrect reminder templates for deleted types on the next self-heal pass', async () => {
|
||||
@@ -118,4 +123,28 @@ describe('event type deletion during the setup window (#800)', () => {
|
||||
|
||||
await db('event_types').where({ id: other.id }).update({ is_active: 1 });
|
||||
});
|
||||
|
||||
it('fails closed when the completion marker row is missing', async () => {
|
||||
// A portable-backup restore can replace app_settings with a set that
|
||||
// predates migration 161 (which will not rerun) — absence must mean
|
||||
// "configured instance", never an open deletion window.
|
||||
await db('app_settings').where({ setting_key: 'setup_wizard_completed' }).del();
|
||||
expect(await setupService.isSetupWizardCompleted()).toBe(true);
|
||||
await setupService.markSetupWizardCompleted();
|
||||
});
|
||||
|
||||
it('refuses to delete the last remaining event type', async () => {
|
||||
// Reduce the catalog to a single custom type via direct db writes (the
|
||||
// service paths are already covered above), then hit the guard.
|
||||
const solo = await eventTypeService.createEventType({ name: 'Solo', slug_prefix: 'solo' });
|
||||
await db('events').del();
|
||||
await db('event_types').whereNot('id', solo.id).del();
|
||||
|
||||
await expect(eventTypeService.deleteEventType(solo.id))
|
||||
.rejects.toMatchObject({ code: 'LAST_TYPE' });
|
||||
|
||||
// Deactivating it would empty the ACTIVE catalog just the same.
|
||||
await expect(eventTypeService.updateEventType(solo.id, { is_active: false }))
|
||||
.rejects.toMatchObject({ code: 'LAST_ACTIVE' });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user