mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
191 lines
6.9 KiB
TypeScript
191 lines
6.9 KiB
TypeScript
/**
|
|
* Unit tests for migration 69/71 — normalized per-user per-channel
|
|
* notification preferences. Covers MIGR-001 to MIGR-004b.
|
|
*
|
|
* Previously a hand-inlined reimplementation of the migration's SQL over a
|
|
* bespoke minimal schema, entirely decoupled from both the legacy runner and
|
|
* the real migration (its `createTables` import was unused dead code). Ported
|
|
* (Task 0/Task 5c triage: PORT — a genuine numbered-migration equivalent
|
|
* exists but had no dedicated test) onto the real
|
|
* `Migration20200101011800_normalized_per_user_per_channel_notification_preferences`:
|
|
* migrate to the step immediately before it, seed rows with raw SQL, apply
|
|
* just that one migration, assert.
|
|
*/
|
|
import { createMigrationOrm, migrateTo, pendingNames, rawExec, rawQuery } from '../../helpers/migration-step';
|
|
import type { MikroORM } from '@mikro-orm/sqlite';
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
|
|
const TARGET = 'Migration20200101011800_normalized_per_user_per_channel_notification_preferences';
|
|
|
|
async function ormBeforeTarget(): Promise<MikroORM> {
|
|
const orm = await createMigrationOrm();
|
|
const names = await pendingNames(orm);
|
|
const idx = names.indexOf(TARGET);
|
|
expect(idx).toBeGreaterThan(0);
|
|
await migrateTo(orm, names[idx - 1]);
|
|
return orm;
|
|
}
|
|
|
|
async function seedUser(orm: MikroORM, username: string): Promise<number> {
|
|
await rawExec(orm, "INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, 'x', 'user')", [
|
|
username,
|
|
`${username}@example.test`,
|
|
]);
|
|
const rows = await rawQuery<{ id: number }>(orm, 'SELECT last_insert_rowid() as id');
|
|
return rows[0].id;
|
|
}
|
|
|
|
describe('Migration 69/71 — normalized notification_channel_preferences', () => {
|
|
it('MIGR-001 — notification_channel_preferences table exists after migration', async () => {
|
|
const orm = await ormBeforeTarget();
|
|
try {
|
|
await migrateTo(orm, TARGET);
|
|
const table = await rawQuery(
|
|
orm,
|
|
`SELECT name FROM sqlite_master WHERE type='table' AND name='notification_channel_preferences'`,
|
|
);
|
|
expect(table[0]).toBeDefined();
|
|
} finally {
|
|
await orm.close(true);
|
|
}
|
|
}, 30000);
|
|
|
|
it('MIGR-002 — old notification_preferences rows with disabled events migrated as enabled=0', async () => {
|
|
const orm = await ormBeforeTarget();
|
|
try {
|
|
const userId = await seedUser(orm, 'testuser');
|
|
// Simulate user who has disabled trip_invite and booking_change email
|
|
await rawExec(
|
|
orm,
|
|
`
|
|
INSERT INTO notification_preferences
|
|
(user_id, notify_trip_invite, notify_booking_change, notify_trip_reminder,
|
|
notify_vacay_invite, notify_photos_shared, notify_collab_message, notify_packing_tagged, notify_webhook)
|
|
VALUES (?, 0, 0, 1, 1, 1, 1, 1, 1)
|
|
`,
|
|
[userId],
|
|
);
|
|
|
|
await migrateTo(orm, TARGET);
|
|
|
|
const read = (eventType: string) =>
|
|
rawQuery<{ enabled: number }>(
|
|
orm,
|
|
'SELECT enabled FROM notification_channel_preferences WHERE user_id = ? AND event_type = ? AND channel = ?',
|
|
[userId, eventType, 'email'],
|
|
).then((r) => r[0]);
|
|
const tripInviteEmail = await read('trip_invite');
|
|
const bookingEmail = await read('booking_change');
|
|
const reminderEmail = await read('trip_reminder');
|
|
|
|
// Disabled events should have enabled=0 rows
|
|
expect(tripInviteEmail).toBeDefined();
|
|
expect(tripInviteEmail!.enabled).toBe(0);
|
|
expect(bookingEmail).toBeDefined();
|
|
expect(bookingEmail!.enabled).toBe(0);
|
|
// Enabled events should have no row (no-row = enabled)
|
|
expect(reminderEmail).toBeUndefined();
|
|
} finally {
|
|
await orm.close(true);
|
|
}
|
|
}, 30000);
|
|
|
|
it('MIGR-003 — old notify_webhook=0 creates disabled webhook rows for all 7 events', async () => {
|
|
const orm = await ormBeforeTarget();
|
|
try {
|
|
const userId = await seedUser(orm, 'webhookuser');
|
|
// User has all email enabled but webhook disabled
|
|
await rawExec(
|
|
orm,
|
|
`
|
|
INSERT INTO notification_preferences
|
|
(user_id, notify_trip_invite, notify_booking_change, notify_trip_reminder,
|
|
notify_vacay_invite, notify_photos_shared, notify_collab_message, notify_packing_tagged, notify_webhook)
|
|
VALUES (?, 1, 1, 1, 1, 1, 1, 1, 0)
|
|
`,
|
|
[userId],
|
|
);
|
|
|
|
await migrateTo(orm, TARGET);
|
|
|
|
const allEvents = [
|
|
'trip_invite',
|
|
'booking_change',
|
|
'trip_reminder',
|
|
'vacay_invite',
|
|
'photos_shared',
|
|
'collab_message',
|
|
'packing_tagged',
|
|
];
|
|
for (const eventType of allEvents) {
|
|
const row = (
|
|
await rawQuery<{ enabled: number }>(
|
|
orm,
|
|
'SELECT enabled FROM notification_channel_preferences WHERE user_id = ? AND event_type = ? AND channel = ?',
|
|
[userId, eventType, 'webhook'],
|
|
)
|
|
)[0];
|
|
expect(row).toBeDefined();
|
|
expect(row!.enabled).toBe(0);
|
|
|
|
// Email rows should NOT exist (all email was enabled → no row needed)
|
|
const emailRow = (
|
|
await rawQuery(
|
|
orm,
|
|
'SELECT enabled FROM notification_channel_preferences WHERE user_id = ? AND event_type = ? AND channel = ?',
|
|
[userId, eventType, 'email'],
|
|
)
|
|
)[0];
|
|
expect(emailRow).toBeUndefined();
|
|
}
|
|
} finally {
|
|
await orm.close(true);
|
|
}
|
|
}, 30000);
|
|
|
|
it('MIGR-004 — notification_channels key is created in app_settings from notification_channel value', async () => {
|
|
const orm = await ormBeforeTarget();
|
|
try {
|
|
// Simulate existing single-channel setting
|
|
await rawExec(orm, 'INSERT INTO app_settings (key, value) VALUES (?, ?)', ['notification_channel', 'email']);
|
|
|
|
await migrateTo(orm, TARGET);
|
|
|
|
const plural = (
|
|
await rawQuery<{ value: string }>(orm, 'SELECT value FROM app_settings WHERE key = ?', [
|
|
'notification_channels',
|
|
])
|
|
)[0];
|
|
expect(plural).toBeDefined();
|
|
expect(plural!.value).toBe('email');
|
|
} finally {
|
|
await orm.close(true);
|
|
}
|
|
}, 30000);
|
|
|
|
it('MIGR-004b — notification_channels is not duplicated if already exists', async () => {
|
|
const orm = await ormBeforeTarget();
|
|
try {
|
|
// Both keys already set (e.g. partial migration or manual edit)
|
|
await rawExec(orm, 'INSERT INTO app_settings (key, value) VALUES (?, ?)', ['notification_channel', 'email']);
|
|
await rawExec(orm, 'INSERT INTO app_settings (key, value) VALUES (?, ?)', [
|
|
'notification_channels',
|
|
'email,webhook',
|
|
]);
|
|
|
|
await migrateTo(orm, TARGET);
|
|
|
|
// The existing notification_channels value should be preserved (INSERT OR IGNORE)
|
|
const plural = (
|
|
await rawQuery<{ value: string }>(orm, 'SELECT value FROM app_settings WHERE key = ?', [
|
|
'notification_channels',
|
|
])
|
|
)[0];
|
|
expect(plural!.value).toBe('email,webhook');
|
|
} finally {
|
|
await orm.close(true);
|
|
}
|
|
}, 30000);
|
|
});
|