diff --git a/server/tests/integration/admin.test.ts b/server/tests/integration/admin.test.ts index 51c72120a..523ccbc44 100644 --- a/server/tests/integration/admin.test.ts +++ b/server/tests/integration/admin.test.ts @@ -23,17 +23,60 @@ vi.mock('../../src/config', () => ({ vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() })); import { db as testDb } from '../../src/db/database'; +import { MikroORM } from '@mikro-orm/core'; import { buildApp } from '../../src/bootstrap'; import { resetTestDb, resetRateLimits } from '../helpers/test-db'; +import type { FactoryOrm } from '../helpers/factories/context'; import { createUser, createAdmin, createInviteToken, createTrip, createBudgetItem, createJourney, createJourneyEntry, addJourneyContributor, addTripPhoto, createCategory, createTag, createTodoItem, createMcpToken, createBucketListItem, createVisitedCountry, createCollabNote, addTripMember } from '../helpers/factories'; import { authCookie } from '../helpers/auth'; +import { findRow, insertRow, insertRowIgnoringConflict, updateRows } from '../helpers/factories/rows'; +import { makeShareToken } from '../helpers/factories/trips'; +import { makeVacayPlan } from '../helpers/factories/vacay'; +import { readUser } from '../helpers/factories/users'; +import { dbNow } from '../../src/db/types/db-timestamp.type'; +import { AuditLog } from '../../src/db/entities/AuditLog.entity'; +import { BucketList } from '../../src/db/entities/BucketList.entity'; +import { BudgetItems } from '../../src/db/entities/BudgetItems.entity'; +import { Categories } from '../../src/db/entities/Categories.entity'; +import { CollabNotes } from '../../src/db/entities/CollabNotes.entity'; +import { InviteTokens } from '../../src/db/entities/InviteTokens.entity'; +import { JourneyContributors } from '../../src/db/entities/JourneyContributors.entity'; +import { JourneyEntries } from '../../src/db/entities/JourneyEntries.entity'; +import { JourneyShareTokens } from '../../src/db/entities/JourneyShareTokens.entity'; +import { Journeys } from '../../src/db/entities/Journeys.entity'; +import { McpTokens } from '../../src/db/entities/McpTokens.entity'; +import { NotificationChannelPreferences } from '../../src/db/entities/NotificationChannelPreferences.entity'; +import { Notifications } from '../../src/db/entities/Notifications.entity'; +import { OauthClients } from '../../src/db/entities/OauthClients.entity'; +import { OauthConsents } from '../../src/db/entities/OauthConsents.entity'; +import { OauthTokens } from '../../src/db/entities/OauthTokens.entity'; +import { PackingBags } from '../../src/db/entities/PackingBags.entity'; +import { PackingTemplates } from '../../src/db/entities/PackingTemplates.entity'; +import { PasswordResetTokens } from '../../src/db/entities/PasswordResetTokens.entity'; +import { Settings } from '../../src/db/entities/Settings.entity'; +import { ShareTokens } from '../../src/db/entities/ShareTokens.entity'; +import { Tags } from '../../src/db/entities/Tags.entity'; +import { TodoItems } from '../../src/db/entities/TodoItems.entity'; +import { TrekPhotos } from '../../src/db/entities/TrekPhotos.entity'; +import { TripFiles } from '../../src/db/entities/TripFiles.entity'; +import { TripMembers } from '../../src/db/entities/TripMembers.entity'; +import { TripPhotos } from '../../src/db/entities/TripPhotos.entity'; +import { Trips } from '../../src/db/entities/Trips.entity'; +import { UserNoticeDismissals } from '../../src/db/entities/UserNoticeDismissals.entity'; +import { Users } from '../../src/db/entities/Users.entity'; +import { VacayPlanMembers } from '../../src/db/entities/VacayPlanMembers.entity'; +import { VacayPlans } from '../../src/db/entities/VacayPlans.entity'; +import { VisitedCountries } from '../../src/db/entities/VisitedCountries.entity'; +import { VisitedRegions } from '../../src/db/entities/VisitedRegions.entity'; let nestApp: INestApplication; let app: Application; +let orm: FactoryOrm; beforeAll(async () => { nestApp = await buildApp(); app = nestApp.getHttpAdapter().getInstance(); + orm = nestApp.get(MikroORM); }); beforeEach(async () => { @@ -128,8 +171,8 @@ describe('Admin user management', () => { expect(res.body.success).toBe(true); // Verify the row is actually gone from the DB - const deleted = testDb.prepare('SELECT id FROM users WHERE id = ?').get(user.id); - expect(deleted).toBeUndefined(); + const deleted = await findRow(orm, Users, { id: user.id }); + expect(deleted).toBeNull(); }); it('ADMIN-005b — DELETE /admin/users/:id succeeds when user has FK references', async () => { @@ -141,14 +184,14 @@ describe('Admin user management', () => { // trip_members.invited_by: target invited thirdUser to otherUser's trip // (trip survives deletion; only invited_by should become NULL) const otherTrip = createTrip(testDb, otherUser.id); - testDb.prepare('INSERT INTO trip_members (trip_id, user_id, invited_by) VALUES (?, ?, ?)').run(otherTrip.id, thirdUser.id, target.id); + await insertRow(orm, TripMembers, { trip: otherTrip.id, user: thirdUser.id, invitedByRef: target.id }); // share_tokens.created_by: target created a share token for otherUser's trip - testDb.prepare("INSERT INTO share_tokens (trip_id, token, created_by) VALUES (?, 'tok-admin-test', ?)").run(otherTrip.id, target.id); + await makeShareToken(orm, otherTrip.id, target.id, { token: 'tok-admin-test' }); // budget_items.paid_by_user_id: target paid for an expense on otherUser's trip const budgetItem = createBudgetItem(testDb, otherTrip.id); - testDb.prepare('UPDATE budget_items SET paid_by_user_id = ? WHERE id = ?').run(target.id, budgetItem.id); + await updateRows(orm, BudgetItems, { id: budgetItem.id }, { paidByUser: target.id }); // journey_contributors: target is a contributor on otherUser's journey const otherJourney = createJourney(testDb, otherUser.id); @@ -158,35 +201,29 @@ describe('Admin user management', () => { createJourneyEntry(testDb, otherJourney.id, target.id); // journey_share_tokens: target created a share token for otherUser's journey - testDb.prepare("INSERT INTO journey_share_tokens (journey_id, token, created_by) VALUES (?, 'jst-admin-test', ?)").run(otherJourney.id, target.id); + await insertRow(orm, JourneyShareTokens, { journey: otherJourney.id, token: 'jst-admin-test', createdByRef: target.id }); // notifications.sender_id (SET NULL): target sent a notification to otherUser - const sentNotif = testDb.prepare( - "INSERT INTO notifications (type, scope, target, sender_id, recipient_id, title_key, text_key) VALUES ('simple', 'trip', ?, ?, ?, 'k', 'k')" - ).run(otherTrip.id, target.id, otherUser.id); + const sentNotif = await insertRow(orm, Notifications, { + type: 'simple', scope: 'trip', target: otherTrip.id, sender: target.id, recipient: otherUser.id, title_key: 'k', text_key: 'k', + }); // notifications.recipient_id (CASCADE): otherUser sent a notification to target - testDb.prepare( - "INSERT INTO notifications (type, scope, target, sender_id, recipient_id, title_key, text_key) VALUES ('simple', 'trip', ?, ?, ?, 'k', 'k')" - ).run(otherTrip.id, otherUser.id, target.id); + await insertRow(orm, Notifications, { + type: 'simple', scope: 'trip', target: otherTrip.id, sender: otherUser.id, recipient: target.id, title_key: 'k', text_key: 'k', + }); // user_notice_dismissals (CASCADE): target dismissed a notice - testDb.prepare( - "INSERT INTO user_notice_dismissals (user_id, notice_id, dismissed_at) VALUES (?, 'test-notice', ?)" - ).run(target.id, Date.now()); + await insertRow(orm, UserNoticeDismissals, { user: target.id, notice_id: 'test-notice', dismissed_at: Date.now() }); // owned journey: target owns a journey with an entry (cascade-deletes on journey deletion) const ownedJourney = createJourney(testDb, target.id); createJourneyEntry(testDb, ownedJourney.id, target.id); // trip_files.uploaded_by (SET NULL): target uploaded a file to otherUser's trip - const fileRow = testDb.prepare( - "INSERT INTO trip_files (trip_id, filename, original_name, uploaded_by) VALUES (?, 'f.pdf', 'file.pdf', ?)" - ).run(otherTrip.id, target.id); + const fileId = await insertRow(orm, TripFiles, { trip: otherTrip.id, filename: 'f.pdf', original_name: 'file.pdf', uploadedByRef: target.id }); // trek_photos.owner_id (SET NULL): target owns a photo in the central registry - const trekPhotoRow = testDb.prepare( - "INSERT INTO trek_photos (provider, asset_id, owner_id) VALUES ('immich', 'asset-admin-test', ?)" - ).run(target.id); + const trekPhotoId = await insertRow(orm, TrekPhotos, { provider: 'immich', asset_id: 'asset-admin-test', owner: target.id }); // trip_photos.user_id (CASCADE): target added a photo to otherUser's trip addTripPhoto(testDb, otherTrip.id, target.id, 'asset-tp-admin', 'immich'); @@ -205,33 +242,33 @@ describe('Admin user management', () => { // todo_items.assigned_user_id (SET NULL): target is assigned to a todo on otherUser's trip const todoItem = createTodoItem(testDb, otherTrip.id); - testDb.prepare('UPDATE todo_items SET assigned_user_id = ? WHERE id = ?').run(target.id, todoItem.id); + await updateRows(orm, TodoItems, { id: todoItem.id }, { assignedUser: target.id }); // packing_bags.user_id (SET NULL): target owns a packing bag on otherUser's trip - const packBagRow = testDb.prepare( - "INSERT INTO packing_bags (trip_id, name, color, user_id) VALUES (?, 'Bag', '#ff0000', ?)" - ).run(otherTrip.id, target.id); + const packBagId = await insertRow(orm, PackingBags, { trip: otherTrip.id, name: 'Bag', color: '#ff0000', user: target.id }); // mcp_tokens.user_id (CASCADE): target has an MCP API token createMcpToken(testDb, target.id); // oauth_tokens/consents.user_id (CASCADE): target has tokens from otherUser's OAuth client - testDb.prepare( - "INSERT INTO oauth_clients (id, user_id, name, client_id, client_secret_hash) VALUES ('cl-admin-test', ?, 'App', 'cid-admin-test', 'h')" - ).run(otherUser.id); - testDb.prepare( - "INSERT INTO oauth_tokens (client_id, user_id, access_token_hash, refresh_token_hash, access_token_expires_at, refresh_token_expires_at) VALUES ('cid-admin-test', ?, 'ath-admin', 'rth-admin', datetime('now','+1 hour'), datetime('now','+30 days'))" - ).run(target.id); - testDb.prepare( - "INSERT INTO oauth_consents (client_id, user_id) VALUES ('cid-admin-test', ?)" - ).run(target.id); + // The row id and the public client_id are the same string here, so the + // tokens' client reference resolves to it whichever of the two it keys on. + await insertRow(orm, OauthClients, { + id: 'cid-admin-test', user: otherUser.id, name: 'App', client_id: 'cid-admin-test', client_secret_hash: 'h', + }); + await insertRow(orm, OauthTokens, { + client: 'cid-admin-test', user: target.id, access_token_hash: 'ath-admin', refresh_token_hash: 'rth-admin', + access_token_expires_at: dbNow(new Date(Date.now() + 60 * 60 * 1000)), + refresh_token_expires_at: dbNow(new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)), + }); + await insertRow(orm, OauthConsents, { client: 'cid-admin-test', user: target.id }); // vacay_plans.owner_id (CASCADE): target owns a vacation plan - const vacayPlanRow = testDb.prepare("INSERT INTO vacay_plans (owner_id) VALUES (?)").run(target.id); + const vacayPlanId = (await makeVacayPlan(orm, target.id)).id; // vacay_plan_members.user_id (CASCADE): target is a member of otherUser's vacay plan - const otherVacayPlanRow = testDb.prepare("INSERT INTO vacay_plans (owner_id) VALUES (?)").run(otherUser.id); - testDb.prepare("INSERT INTO vacay_plan_members (plan_id, user_id) VALUES (?, ?)").run(otherVacayPlanRow.lastInsertRowid, target.id); + const otherVacayPlanId = (await makeVacayPlan(orm, otherUser.id)).id; + await insertRow(orm, VacayPlanMembers, { plan: otherVacayPlanId, user: target.id }); // bucket_list.user_id (CASCADE): target has a bucket list item createBucketListItem(testDb, target.id); @@ -240,14 +277,10 @@ describe('Admin user management', () => { createVisitedCountry(testDb, target.id, 'JP'); // visited_regions.user_id (CASCADE): target has visited a region - testDb.prepare( - "INSERT INTO visited_regions (user_id, region_code, region_name, country_code) VALUES (?, 'JP-13', 'Tokyo', 'JP')" - ).run(target.id); + await insertRow(orm, VisitedRegions, { user: target.id, region_code: 'JP-13', region_name: 'Tokyo', country_code: 'JP' }); // packing_templates.created_by (CASCADE): target created a packing template - const packTemplateRow = testDb.prepare( - "INSERT INTO packing_templates (name, created_by) VALUES ('My Template', ?)" - ).run(target.id); + const packTemplateId = await insertRow(orm, PackingTemplates, { name: 'My Template', createdByRef: target.id }); // invite_tokens.created_by (CASCADE): target created an invite token createInviteToken(testDb, { created_by: target.id }); @@ -256,20 +289,18 @@ describe('Admin user management', () => { createCollabNote(testDb, otherTrip.id, target.id); // settings.user_id (CASCADE): target has a user setting - testDb.prepare("INSERT INTO settings (user_id, key, value) VALUES (?, 'theme', 'dark')").run(target.id); + await insertRow(orm, Settings, { user: target.id, key: 'theme', value: 'dark' }); // password_reset_tokens.user_id (CASCADE): target has a pending password reset - testDb.prepare( - "INSERT INTO password_reset_tokens (user_id, token_hash, expires_at) VALUES (?, 'prt-hash-admin', datetime('now','+1 hour'))" - ).run(target.id); + await insertRow(orm, PasswordResetTokens, { + user: target.id, token_hash: 'prt-hash-admin', expires_at: dbNow(new Date(Date.now() + 60 * 60 * 1000)), + }); // audit_log.user_id (SET NULL): target performed an audited action - const auditRow = testDb.prepare( - "INSERT INTO audit_log (user_id, action, ip) VALUES (?, 'test.action', '127.0.0.1')" - ).run(target.id); + const auditId = await insertRow(orm, AuditLog, { user: target.id, action: 'test.action', ip: '127.0.0.1' }); // notification_channel_preferences.user_id (CASCADE): target has notification preferences - testDb.prepare("INSERT OR IGNORE INTO notification_channel_preferences (user_id, event_type, channel) VALUES (?, 'trip_invite', 'email')").run(target.id); + await insertRowIgnoringConflict(orm, NotificationChannelPreferences, { user: target.id, event_type: 'trip_invite', channel: 'email' }); const res = await request(app) .delete(`/api/admin/users/${target.id}`) @@ -277,69 +308,69 @@ describe('Admin user management', () => { expect(res.status).toBe(200); expect(res.body.success).toBe(true); - expect(testDb.prepare('SELECT id FROM users WHERE id = ?').get(target.id)).toBeUndefined(); +expect(await findRow(orm, Users, { id: target.id })).toBeNull(); // trip_members row survives but invited_by is now NULL - expect((testDb.prepare('SELECT invited_by FROM trip_members WHERE trip_id = ? AND user_id = ?').get(otherTrip.id, thirdUser.id) as any).invited_by).toBeNull(); - expect(testDb.prepare('SELECT id FROM share_tokens WHERE created_by = ?').get(target.id)).toBeUndefined(); - expect((testDb.prepare('SELECT paid_by_user_id FROM budget_items WHERE id = ?').get(budgetItem.id) as any).paid_by_user_id).toBeNull(); - expect(testDb.prepare('SELECT user_id FROM journey_contributors WHERE journey_id = ? AND user_id = ?').get(otherJourney.id, target.id)).toBeUndefined(); - expect(testDb.prepare('SELECT id FROM journey_entries WHERE author_id = ?').get(target.id)).toBeUndefined(); - expect(testDb.prepare('SELECT id FROM journey_share_tokens WHERE created_by = ?').get(target.id)).toBeUndefined(); + expect((await findRow(orm, TripMembers, { trip: otherTrip.id, user: thirdUser.id }))?.invited_by).toBeNull(); +expect(await findRow(orm, ShareTokens, { createdByRef: target.id })).toBeNull(); + expect((await findRow(orm, BudgetItems, { id: budgetItem.id }))?.paid_by_user_id).toBeNull(); +expect(await findRow(orm, JourneyContributors, { journey: otherJourney.id, user: target.id })).toBeNull(); +expect(await findRow(orm, JourneyEntries, { author: target.id })).toBeNull(); +expect(await findRow(orm, JourneyShareTokens, { createdByRef: target.id })).toBeNull(); // sent notification survives but sender_id becomes NULL - expect((testDb.prepare('SELECT sender_id FROM notifications WHERE id = ?').get(sentNotif.lastInsertRowid) as any).sender_id).toBeNull(); + expect((await findRow(orm, Notifications, { id: sentNotif }))?.sender_id).toBeNull(); // received notification is cascade-deleted - expect(testDb.prepare('SELECT id FROM notifications WHERE recipient_id = ?').get(target.id)).toBeUndefined(); +expect(await findRow(orm, Notifications, { recipient: target.id })).toBeNull(); // notice dismissals are cascade-deleted - expect(testDb.prepare("SELECT user_id FROM user_notice_dismissals WHERE user_id = ? AND notice_id = 'test-notice'").get(target.id)).toBeUndefined(); +expect(await findRow(orm, UserNoticeDismissals, { user: target.id, notice_id: 'test-notice' })).toBeNull(); // owned journey and its entries are cascade-deleted - expect(testDb.prepare('SELECT id FROM journeys WHERE user_id = ?').get(target.id)).toBeUndefined(); - expect(testDb.prepare('SELECT id FROM journey_entries WHERE journey_id = ?').get(ownedJourney.id)).toBeUndefined(); +expect(await findRow(orm, Journeys, { user: target.id })).toBeNull(); +expect(await findRow(orm, JourneyEntries, { journey: ownedJourney.id })).toBeNull(); // uploaded file survives but uploaded_by is now NULL - expect((testDb.prepare('SELECT uploaded_by FROM trip_files WHERE id = ?').get(fileRow.lastInsertRowid) as any).uploaded_by).toBeNull(); + expect((await findRow(orm, TripFiles, { id: fileId }))?.uploaded_by).toBeNull(); // trek_photos row survives but owner_id is now NULL - expect((testDb.prepare('SELECT owner_id FROM trek_photos WHERE id = ?').get(trekPhotoRow.lastInsertRowid) as any).owner_id).toBeNull(); + expect((await findRow(orm, TrekPhotos, { id: trekPhotoId }))?.owner_id).toBeNull(); // trip_photos row for target is cascade-deleted - expect(testDb.prepare("SELECT id FROM trip_photos WHERE trip_id = ? AND user_id = ?").get(otherTrip.id, target.id)).toBeUndefined(); +expect(await findRow(orm, TripPhotos, { trip: otherTrip.id, user: target.id })).toBeNull(); // owned trip is cascade-deleted - expect(testDb.prepare('SELECT id FROM trips WHERE id = ?').get(ownedTrip.id)).toBeUndefined(); +expect(await findRow(orm, Trips, { id: ownedTrip.id })).toBeNull(); // trip membership on others' trips is removed - expect(testDb.prepare('SELECT id FROM trip_members WHERE trip_id = ? AND user_id = ?').get(otherTrip.id, target.id)).toBeUndefined(); +expect(await findRow(orm, TripMembers, { trip: otherTrip.id, user: target.id })).toBeNull(); // category survives but user_id is NULL - expect((testDb.prepare('SELECT user_id FROM categories WHERE id = ?').get(userCategory.id) as any).user_id).toBeNull(); + expect((await findRow(orm, Categories, { id: userCategory.id }))?.user_id).toBeNull(); // tag is deleted - expect(testDb.prepare('SELECT id FROM tags WHERE id = ?').get(userTag.id)).toBeUndefined(); +expect(await findRow(orm, Tags, { id: userTag.id })).toBeNull(); // todo assigned_user_id is NULL - expect((testDb.prepare('SELECT assigned_user_id FROM todo_items WHERE id = ?').get(todoItem.id) as any).assigned_user_id).toBeNull(); + expect((await findRow(orm, TodoItems, { id: todoItem.id }))?.assigned_user_id).toBeNull(); // packing bag survives but user_id is NULL - expect((testDb.prepare('SELECT user_id FROM packing_bags WHERE id = ?').get(packBagRow.lastInsertRowid) as any).user_id).toBeNull(); + expect((await findRow(orm, PackingBags, { id: packBagId }))?.user_id).toBeNull(); // MCP tokens are deleted - expect(testDb.prepare('SELECT id FROM mcp_tokens WHERE user_id = ?').get(target.id)).toBeUndefined(); +expect(await findRow(orm, McpTokens, { user: target.id })).toBeNull(); // OAuth tokens and consents are deleted - expect(testDb.prepare('SELECT id FROM oauth_tokens WHERE user_id = ?').get(target.id)).toBeUndefined(); - expect(testDb.prepare('SELECT id FROM oauth_consents WHERE user_id = ?').get(target.id)).toBeUndefined(); + expect(await findRow(orm, OauthTokens, { user: target.id })).toBeNull(); + expect(await findRow(orm, OauthConsents, { user: target.id })).toBeNull(); // owned vacay plan is deleted - expect(testDb.prepare('SELECT id FROM vacay_plans WHERE id = ?').get(vacayPlanRow.lastInsertRowid)).toBeUndefined(); +expect(await findRow(orm, VacayPlans, { id: vacayPlanId })).toBeNull(); // vacay plan membership on others' plans is removed - expect(testDb.prepare('SELECT id FROM vacay_plan_members WHERE plan_id = ? AND user_id = ?').get(otherVacayPlanRow.lastInsertRowid, target.id)).toBeUndefined(); +expect(await findRow(orm, VacayPlanMembers, { plan: otherVacayPlanId, user: target.id })).toBeNull(); // bucket list items are deleted - expect(testDb.prepare('SELECT id FROM bucket_list WHERE user_id = ?').get(target.id)).toBeUndefined(); +expect(await findRow(orm, BucketList, { user: target.id })).toBeNull(); // travel history is deleted - expect(testDb.prepare('SELECT user_id FROM visited_countries WHERE user_id = ? AND country_code = ?').get(target.id, 'JP')).toBeUndefined(); - expect(testDb.prepare('SELECT id FROM visited_regions WHERE user_id = ?').get(target.id)).toBeUndefined(); + expect(await findRow(orm, VisitedCountries, { user: target.id, country_code: 'JP' })).toBeNull(); + expect(await findRow(orm, VisitedRegions, { user: target.id })).toBeNull(); // packing template is deleted - expect(testDb.prepare('SELECT id FROM packing_templates WHERE id = ?').get(packTemplateRow.lastInsertRowid)).toBeUndefined(); +expect(await findRow(orm, PackingTemplates, { id: packTemplateId })).toBeNull(); // invite tokens created by target are deleted - expect(testDb.prepare('SELECT id FROM invite_tokens WHERE created_by = ?').get(target.id)).toBeUndefined(); +expect(await findRow(orm, InviteTokens, { createdByRef: target.id })).toBeNull(); // collab content is deleted - expect(testDb.prepare('SELECT id FROM collab_notes WHERE user_id = ? AND trip_id = ?').get(target.id, otherTrip.id)).toBeUndefined(); +expect(await findRow(orm, CollabNotes, { user: target.id, trip: otherTrip.id })).toBeNull(); // user settings are deleted - expect(testDb.prepare("SELECT id FROM settings WHERE user_id = ?").get(target.id)).toBeUndefined(); +expect(await findRow(orm, Settings, { user: target.id })).toBeNull(); // password reset tokens are deleted - expect(testDb.prepare('SELECT id FROM password_reset_tokens WHERE user_id = ?').get(target.id)).toBeUndefined(); +expect(await findRow(orm, PasswordResetTokens, { user: target.id })).toBeNull(); // audit log entry survives but user_id is NULL - expect((testDb.prepare('SELECT user_id FROM audit_log WHERE id = ?').get(auditRow.lastInsertRowid) as any).user_id).toBeNull(); + expect((await findRow(orm, AuditLog, { id: auditId }))?.user_id).toBeNull(); // notification channel preferences are deleted - expect(testDb.prepare("SELECT user_id FROM notification_channel_preferences WHERE user_id = ? AND event_type = 'trip_invite'").get(target.id)).toBeUndefined(); +expect(await findRow(orm, NotificationChannelPreferences, { user: target.id, event_type: 'trip_invite' })).toBeNull(); }); it('ADMIN-006 — admin cannot delete their own account', async () => { @@ -367,7 +398,7 @@ describe('Admin user management — whitespace normalization', () => { .send({ username: ' trimmedadmin ' }); expect(res.status).toBe(200); - const row = testDb.prepare('SELECT username FROM users WHERE id = ?').get(user.id) as { username: string }; + const row = await readUser(orm, user.id); expect(row.username).toBe('trimmedadmin'); }); @@ -381,7 +412,7 @@ describe('Admin user management — whitespace normalization', () => { .send({ email: ' newemail@example.com ' }); expect(res.status).toBe(200); - const row = testDb.prepare('SELECT email FROM users WHERE id = ?').get(user.id) as { email: string }; + const row = await readUser(orm, user.id); expect(row.email).toBe('newemail@example.com'); });