chore: apply prettier on the entire project

This commit is contained in:
jubnl
2026-05-25 21:59:42 +02:00
parent c130ed41be
commit 6bcdfbc34b
488 changed files with 82986 additions and 45830 deletions
+228 -141
View File
@@ -4,10 +4,37 @@
* OIDC scenarios (AUTH-023 to AUTH-027) require a real IdP and are excluded.
* Rate limiting scenarios (AUTH-004, AUTH-018) are at the end of this file.
*/
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
import request from 'supertest';
import { createApp } from '../../src/app';
import { runMigrations } from '../../src/db/migrations';
import { createTables } from '../../src/db/schema';
import { loginAttempts, mfaAttempts } from '../../src/routes/auth';
import { authCookie, authHeader } from '../helpers/auth';
import {
createUser,
createAdmin,
createUserWithMfa,
createInviteToken,
createTrip,
createBudgetItem,
createJourney,
createJourneyEntry,
addJourneyContributor,
addTripPhoto,
createCategory,
createTag,
createTodoItem,
createMcpToken,
createBucketListItem,
createVisitedCountry,
createCollabNote,
addTripMember,
} from '../helpers/factories';
import { resetTestDb } from '../helpers/test-db';
import type { Application } from 'express';
import { authenticator } from 'otplib';
import request from 'supertest';
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
// ─────────────────────────────────────────────────────────────────────────────
// Step 1: Bare in-memory DB — schema applied in beforeAll after mocks register
@@ -24,16 +51,32 @@ const { testDb, dbMock } = vi.hoisted(() => {
closeDb: () => {},
reinitialize: () => {},
getPlaceWithTags: (placeId: number) => {
const place: any = db.prepare(`
const place: any = db
.prepare(
`
SELECT p.*, c.name as category_name, c.color as category_color, c.icon as category_icon
FROM places p LEFT JOIN categories c ON p.category_id = c.id WHERE p.id = ?
`).get(placeId);
`,
)
.get(placeId);
if (!place) return null;
const tags = db.prepare(`SELECT t.* FROM tags t JOIN place_tags pt ON t.id = pt.tag_id WHERE pt.place_id = ?`).all(placeId);
return { ...place, category: place.category_id ? { id: place.category_id, name: place.category_name, color: place.category_color, icon: place.category_icon } : null, tags };
const tags = db
.prepare(`SELECT t.* FROM tags t JOIN place_tags pt ON t.id = pt.tag_id WHERE pt.place_id = ?`)
.all(placeId);
return {
...place,
category: place.category_id
? { id: place.category_id, name: place.category_name, color: place.category_color, icon: place.category_icon }
: null,
tags,
};
},
canAccessTrip: (tripId: any, userId: number) =>
db.prepare(`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`).get(userId, tripId, userId),
db
.prepare(
`SELECT t.id, t.user_id FROM trips t LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ? WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)`,
)
.get(userId, tripId, userId),
isOwner: (tripId: any, userId: number) =>
!!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId),
};
@@ -48,14 +91,6 @@ vi.mock('../../src/config', () => ({
updateJwtSecret: () => {},
}));
import { createApp } from '../../src/app';
import { createTables } from '../../src/db/schema';
import { runMigrations } from '../../src/db/migrations';
import { resetTestDb } from '../helpers/test-db';
import { createUser, createAdmin, createUserWithMfa, createInviteToken, createTrip, createBudgetItem, createJourney, createJourneyEntry, addJourneyContributor, addTripPhoto, createCategory, createTag, createTodoItem, createMcpToken, createBucketListItem, createVisitedCountry, createCollabNote, addTripMember } from '../helpers/factories';
import { authCookie, authHeader } from '../helpers/auth';
import { loginAttempts, mfaAttempts } from '../../src/routes/auth';
const app: Application = createApp();
beforeAll(() => {
@@ -100,7 +135,9 @@ describe('Login', () => {
});
it('AUTH-003 — non-existent email returns 401 with same generic message (no user enumeration)', async () => {
const res = await request(app).post('/api/auth/login').send({ email: 'nobody@example.com', password: 'SomePass1!' });
const res = await request(app)
.post('/api/auth/login')
.send({ email: 'nobody@example.com', password: 'SomePass1!' });
expect(res.status).toBe(401);
// Must be same message as wrong-password to avoid email enumeration
expect(res.body.error).toContain('Invalid email or password');
@@ -111,7 +148,9 @@ describe('Login', () => {
expect(res.status).toBe(200);
const cookies: string[] = Array.isArray(res.headers['set-cookie'])
? res.headers['set-cookie']
: (res.headers['set-cookie'] ? [res.headers['set-cookie']] : []);
: res.headers['set-cookie']
? [res.headers['set-cookie']]
: [];
const sessionCookie = cookies.find((c: string) => c.includes('trek_session'));
expect(sessionCookie).toBeDefined();
expect(sessionCookie).toMatch(/expires=Thu, 01 Jan 1970|Max-Age=0/i);
@@ -192,7 +231,9 @@ describe('Registration', () => {
});
expect(res.status).toBe(201);
const row = testDb.prepare('SELECT used_count FROM invite_tokens WHERE id = ?').get(invite.id) as { used_count: number };
const row = testDb.prepare('SELECT used_count FROM invite_tokens WHERE id = ?').get(invite.id) as {
used_count: number;
};
expect(row.used_count).toBe(1);
});
@@ -230,7 +271,9 @@ describe('Registration — whitespace normalization', () => {
password: 'Str0ng!Pass',
});
expect(res.status).toBe(201);
const row = testDb.prepare('SELECT username FROM users WHERE email = ?').get('trimmed@example.com') as { username: string };
const row = testDb.prepare('SELECT username FROM users WHERE email = ?').get('trimmed@example.com') as {
username: string;
};
expect(row.username).toBe('trimmeduser');
});
@@ -333,9 +376,9 @@ describe('Demo login', () => {
});
it('AUTH-022 — POST /api/auth/demo-login with DEMO_MODE and demo user returns 200 + cookie', async () => {
testDb.prepare(
"INSERT INTO users (username, email, password_hash, role) VALUES ('demo', 'demo@trek.app', 'x', 'user')"
).run();
testDb
.prepare("INSERT INTO users (username, email, password_hash, role) VALUES ('demo', 'demo@trek.app', 'x', 'user')")
.run();
process.env.DEMO_MODE = 'true';
try {
const res = await request(app).post('/api/auth/demo-login');
@@ -354,9 +397,7 @@ describe('Demo login', () => {
describe('MFA', () => {
it('AUTH-015 — POST /api/auth/mfa/setup returns secret and QR data URL', async () => {
const { user } = createUser(testDb);
const res = await request(app)
.post('/api/auth/mfa/setup')
.set('Cookie', authCookie(user.id));
const res = await request(app).post('/api/auth/mfa/setup').set('Cookie', authCookie(user.id));
expect(res.status).toBe(200);
expect(res.body.secret).toBeDefined();
expect(res.body.otpauth_url).toContain('otpauth://');
@@ -366,9 +407,7 @@ describe('MFA', () => {
it('AUTH-015 — POST /api/auth/mfa/enable with valid TOTP code enables MFA', async () => {
const { user } = createUser(testDb);
const setupRes = await request(app)
.post('/api/auth/mfa/setup')
.set('Cookie', authCookie(user.id));
const setupRes = await request(app).post('/api/auth/mfa/setup').set('Cookie', authCookie(user.id));
expect(setupRes.status).toBe(200);
const enableRes = await request(app)
@@ -382,9 +421,7 @@ describe('MFA', () => {
it('AUTH-016 — login with MFA-enabled account returns mfa_required + mfa_token', async () => {
const { user, password } = createUserWithMfa(testDb);
const loginRes = await request(app)
.post('/api/auth/login')
.send({ email: user.email, password });
const loginRes = await request(app).post('/api/auth/login').send({ email: user.email, password });
expect(loginRes.status).toBe(200);
expect(loginRes.body.mfa_required).toBe(true);
expect(typeof loginRes.body.mfa_token).toBe('string');
@@ -393,9 +430,7 @@ describe('MFA', () => {
it('AUTH-016 — POST /api/auth/mfa/verify-login with valid code completes login', async () => {
const { user, password, totpSecret } = createUserWithMfa(testDb);
const loginRes = await request(app)
.post('/api/auth/login')
.send({ email: user.email, password });
const loginRes = await request(app).post('/api/auth/login').send({ email: user.email, password });
const { mfa_token } = loginRes.body;
const verifyRes = await request(app)
@@ -411,9 +446,7 @@ describe('MFA', () => {
it('AUTH-017 — verify-login with invalid TOTP code returns 401', async () => {
const { user, password } = createUserWithMfa(testDb);
const loginRes = await request(app)
.post('/api/auth/login')
.send({ email: user.email, password });
const loginRes = await request(app).post('/api/auth/login').send({ email: user.email, password });
const verifyRes = await request(app)
.post('/api/auth/mfa/verify-login')
@@ -476,9 +509,7 @@ describe('Forced MFA policy', () => {
describe('Short-lived tokens', () => {
it('AUTH-029 — POST /api/auth/ws-token returns a single-use token', async () => {
const { user } = createUser(testDb);
const res = await request(app)
.post('/api/auth/ws-token')
.set('Cookie', authCookie(user.id));
const res = await request(app).post('/api/auth/ws-token').set('Cookie', authCookie(user.id));
expect(res.status).toBe(200);
expect(typeof res.body.token).toBe('string');
expect(res.body.token.length).toBeGreaterThan(0);
@@ -504,9 +535,7 @@ describe('Extended auth scenarios', () => {
it('AUTH-031 — login succeeds with uppercased email (case-insensitive lookup)', async () => {
const { user, password } = createUser(testDb, { email: 'alice@example.com' });
const res = await request(app)
.post('/api/auth/login')
.send({ email: 'ALICE@EXAMPLE.COM', password });
const res = await request(app).post('/api/auth/login').send({ email: 'ALICE@EXAMPLE.COM', password });
expect(res.status).toBe(200);
expect(res.body.user).toBeDefined();
});
@@ -527,27 +556,20 @@ describe('Extended auth scenarios', () => {
// Generate and store backup codes on the MFA-enabled user
const backupCodes = generateBackupCodes();
const backupHashes = backupCodes.map(hashBackupCode);
testDb.prepare('UPDATE users SET mfa_backup_codes = ? WHERE id = ?')
.run(JSON.stringify(backupHashes), user.id);
testDb.prepare('UPDATE users SET mfa_backup_codes = ? WHERE id = ?').run(JSON.stringify(backupHashes), user.id);
// Step 1: login to get mfa_token
const loginRes = await request(app)
.post('/api/auth/login')
.send({ email: user.email, password });
const loginRes = await request(app).post('/api/auth/login').send({ email: user.email, password });
expect(loginRes.body.mfa_required).toBe(true);
const { mfa_token } = loginRes.body;
// Step 2: verify with a backup code
const res = await request(app)
.post('/api/auth/mfa/verify-login')
.send({ mfa_token, code: backupCodes[0] });
const res = await request(app).post('/api/auth/mfa/verify-login').send({ mfa_token, code: backupCodes[0] });
expect(res.status).toBe(200);
expect(res.body.user).toBeDefined();
// Step 3: same backup code is now consumed — second login attempt fails
const loginRes2 = await request(app)
.post('/api/auth/login')
.send({ email: user.email, password });
const loginRes2 = await request(app).post('/api/auth/login').send({ email: user.email, password });
const { mfa_token: mfa_token2 } = loginRes2.body;
const res2 = await request(app)
@@ -571,10 +593,14 @@ describe('Account deletion', () => {
// 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);
testDb
.prepare('INSERT INTO trip_members (trip_id, user_id, invited_by) VALUES (?, ?, ?)')
.run(otherTrip.id, thirdUser.id, 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-auth-test', ?)").run(otherTrip.id, target.id);
testDb
.prepare("INSERT INTO share_tokens (trip_id, token, created_by) VALUES (?, 'tok-auth-test', ?)")
.run(otherTrip.id, target.id);
// budget_items.paid_by_user_id: target paid for an expense on otherUser's trip
const budgetItem = createBudgetItem(testDb, otherTrip.id);
@@ -588,35 +614,43 @@ describe('Account deletion', () => {
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-auth-test', ?)").run(otherJourney.id, target.id);
testDb
.prepare("INSERT INTO journey_share_tokens (journey_id, token, created_by) VALUES (?, 'jst-auth-test', ?)")
.run(otherJourney.id, 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 = 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);
// 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);
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);
// 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());
testDb
.prepare("INSERT INTO user_notice_dismissals (user_id, notice_id, dismissed_at) VALUES (?, 'test-notice', ?)")
.run(target.id, 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 fileRow = testDb
.prepare(
"INSERT INTO trip_files (trip_id, filename, original_name, uploaded_by) VALUES (?, 'f.pdf', 'file.pdf', ?)",
)
.run(otherTrip.id, 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-auth-test', ?)"
).run(target.id);
const trekPhotoRow = testDb
.prepare("INSERT INTO trek_photos (provider, asset_id, owner_id) VALUES ('immich', 'asset-auth-test', ?)")
.run(target.id);
// trip_photos.user_id (CASCADE): target added a photo to otherUser's trip
addTripPhoto(testDb, otherTrip.id, target.id, 'asset-tp-auth', 'immich');
@@ -638,30 +672,34 @@ describe('Account deletion', () => {
testDb.prepare('UPDATE todo_items SET assigned_user_id = ? WHERE id = ?').run(target.id, todoItem.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 packBagRow = testDb
.prepare("INSERT INTO packing_bags (trip_id, name, color, user_id) VALUES (?, 'Bag', '#ff0000', ?)")
.run(otherTrip.id, 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-auth-test', ?, 'App', 'cid-auth-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-auth-test', ?, 'ath-auth', 'rth-auth', datetime('now','+1 hour'), datetime('now','+30 days'))"
).run(target.id);
testDb.prepare(
"INSERT INTO oauth_consents (client_id, user_id) VALUES ('cid-auth-test', ?)"
).run(target.id);
testDb
.prepare(
"INSERT INTO oauth_clients (id, user_id, name, client_id, client_secret_hash) VALUES ('cl-auth-test', ?, 'App', 'cid-auth-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-auth-test', ?, 'ath-auth', 'rth-auth', datetime('now','+1 hour'), datetime('now','+30 days'))",
)
.run(target.id);
testDb.prepare("INSERT INTO oauth_consents (client_id, user_id) VALUES ('cid-auth-test', ?)").run(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 vacayPlanRow = testDb.prepare('INSERT INTO vacay_plans (owner_id) VALUES (?)').run(target.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 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);
// bucket_list.user_id (CASCADE): target has a bucket list item
createBucketListItem(testDb, target.id);
@@ -670,14 +708,16 @@ describe('Account deletion', () => {
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);
testDb
.prepare(
"INSERT INTO visited_regions (user_id, region_code, region_name, country_code) VALUES (?, 'JP-13', 'Tokyo', 'JP')",
)
.run(target.id);
// 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 packTemplateRow = testDb
.prepare("INSERT INTO packing_templates (name, created_by) VALUES ('My Template', ?)")
.run(target.id);
// invite_tokens.created_by (CASCADE): target created an invite token
createInviteToken(testDb, { created_by: target.id });
@@ -689,62 +729,102 @@ describe('Account deletion', () => {
testDb.prepare("INSERT INTO settings (user_id, key, value) VALUES (?, 'theme', 'dark')").run(target.id);
// 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-auth', datetime('now','+1 hour'))"
).run(target.id);
testDb
.prepare(
"INSERT INTO password_reset_tokens (user_id, token_hash, expires_at) VALUES (?, 'prt-hash-auth', datetime('now','+1 hour'))",
)
.run(target.id);
// 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 auditRow = testDb
.prepare("INSERT INTO audit_log (user_id, action, ip) VALUES (?, 'test.action', '127.0.0.1')")
.run(target.id);
// 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);
testDb
.prepare(
"INSERT OR IGNORE INTO notification_channel_preferences (user_id, event_type, channel) VALUES (?, 'trip_invite', 'email')",
)
.run(target.id);
// admin exists to ensure target (non-admin user) passes the last-admin guard
void admin;
const res = await request(app)
.delete('/api/auth/me')
.set('Cookie', authCookie(target.id));
const res = await request(app).delete('/api/auth/me').set('Cookie', authCookie(target.id));
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(testDb.prepare('SELECT id FROM users WHERE id = ?').get(target.id)).toBeUndefined();
// 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 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 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();
// 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(
(testDb.prepare('SELECT sender_id FROM notifications WHERE id = ?').get(sentNotif.lastInsertRowid) as any)
.sender_id,
).toBeNull();
// received notification is cascade-deleted
expect(testDb.prepare('SELECT id FROM notifications WHERE recipient_id = ?').get(target.id)).toBeUndefined();
// 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(
testDb
.prepare("SELECT user_id FROM user_notice_dismissals WHERE user_id = ? AND notice_id = 'test-notice'")
.get(target.id),
).toBeUndefined();
// 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();
// 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(
(testDb.prepare('SELECT uploaded_by FROM trip_files WHERE id = ?').get(fileRow.lastInsertRowid) as any)
.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(
(testDb.prepare('SELECT owner_id FROM trek_photos WHERE id = ?').get(trekPhotoRow.lastInsertRowid) as any)
.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(
testDb.prepare('SELECT id FROM trip_photos WHERE trip_id = ? AND user_id = ?').get(otherTrip.id, target.id),
).toBeUndefined();
// owned trip is cascade-deleted
expect(testDb.prepare('SELECT id FROM trips WHERE id = ?').get(ownedTrip.id)).toBeUndefined();
// 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(
testDb.prepare('SELECT id FROM trip_members WHERE trip_id = ? AND user_id = ?').get(otherTrip.id, target.id),
).toBeUndefined();
// 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(
(testDb.prepare('SELECT user_id FROM categories WHERE id = ?').get(userCategory.id) as any).user_id,
).toBeNull();
// tag is deleted
expect(testDb.prepare('SELECT id FROM tags WHERE id = ?').get(userTag.id)).toBeUndefined();
// 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(
(testDb.prepare('SELECT assigned_user_id FROM todo_items WHERE id = ?').get(todoItem.id) as any).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(
(testDb.prepare('SELECT user_id FROM packing_bags WHERE id = ?').get(packBagRow.lastInsertRowid) as any).user_id,
).toBeNull();
// MCP tokens are deleted
expect(testDb.prepare('SELECT id FROM mcp_tokens WHERE user_id = ?').get(target.id)).toBeUndefined();
// OAuth tokens and consents are deleted
@@ -753,26 +833,46 @@ describe('Account deletion', () => {
// owned vacay plan is deleted
expect(testDb.prepare('SELECT id FROM vacay_plans WHERE id = ?').get(vacayPlanRow.lastInsertRowid)).toBeUndefined();
// 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(
testDb
.prepare('SELECT id FROM vacay_plan_members WHERE plan_id = ? AND user_id = ?')
.get(otherVacayPlanRow.lastInsertRowid, target.id),
).toBeUndefined();
// bucket list items are deleted
expect(testDb.prepare('SELECT id FROM bucket_list WHERE user_id = ?').get(target.id)).toBeUndefined();
// 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 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();
// packing template is deleted
expect(testDb.prepare('SELECT id FROM packing_templates WHERE id = ?').get(packTemplateRow.lastInsertRowid)).toBeUndefined();
expect(
testDb.prepare('SELECT id FROM packing_templates WHERE id = ?').get(packTemplateRow.lastInsertRowid),
).toBeUndefined();
// invite tokens created by target are deleted
expect(testDb.prepare('SELECT id FROM invite_tokens WHERE created_by = ?').get(target.id)).toBeUndefined();
// 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(
testDb.prepare('SELECT id FROM collab_notes WHERE user_id = ? AND trip_id = ?').get(target.id, otherTrip.id),
).toBeUndefined();
// user settings are deleted
expect(testDb.prepare("SELECT id FROM settings WHERE user_id = ?").get(target.id)).toBeUndefined();
expect(testDb.prepare('SELECT id FROM settings WHERE user_id = ?').get(target.id)).toBeUndefined();
// password reset tokens are deleted
expect(testDb.prepare('SELECT id FROM password_reset_tokens WHERE user_id = ?').get(target.id)).toBeUndefined();
// 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(
(testDb.prepare('SELECT user_id FROM audit_log WHERE id = ?').get(auditRow.lastInsertRowid) as any).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(
testDb
.prepare(
"SELECT user_id FROM notification_channel_preferences WHERE user_id = ? AND event_type = 'trip_invite'",
)
.get(target.id),
).toBeUndefined();
});
});
@@ -797,9 +897,7 @@ describe('Rate limiting', () => {
it('AUTH-018 — MFA verify-login endpoint rate-limits after 5 attempts', async () => {
let lastStatus = 0;
for (let i = 0; i <= 5; i++) {
const res = await request(app)
.post('/api/auth/mfa/verify-login')
.send({ mfa_token: 'badtoken', code: '000000' });
const res = await request(app).post('/api/auth/mfa/verify-login').send({ mfa_token: 'badtoken', code: '000000' });
lastStatus = res.status;
if (lastStatus === 429) break;
}
@@ -814,9 +912,7 @@ describe('Rate limiting', () => {
describe('MCP token management', () => {
it('AUTH-034 — GET /auth/mcp-tokens returns empty list initially', async () => {
const { user } = createUser(testDb);
const res = await request(app)
.get('/api/auth/mcp-tokens')
.set('Cookie', authCookie(user.id));
const res = await request(app).get('/api/auth/mcp-tokens').set('Cookie', authCookie(user.id));
expect(res.status).toBe(200);
expect(res.body.tokens).toEqual([]);
});
@@ -834,10 +930,7 @@ describe('MCP token management', () => {
it('AUTH-036 — POST /auth/mcp-tokens without name returns 400', async () => {
const { user } = createUser(testDb);
const res = await request(app)
.post('/api/auth/mcp-tokens')
.set('Cookie', authCookie(user.id))
.send({});
const res = await request(app).post('/api/auth/mcp-tokens').set('Cookie', authCookie(user.id)).send({});
expect(res.status).toBe(400);
});
@@ -850,23 +943,17 @@ describe('MCP token management', () => {
expect(createRes.status).toBe(201);
const tokenId = createRes.body.token.id;
const delRes = await request(app)
.delete(`/api/auth/mcp-tokens/${tokenId}`)
.set('Cookie', authCookie(user.id));
const delRes = await request(app).delete(`/api/auth/mcp-tokens/${tokenId}`).set('Cookie', authCookie(user.id));
expect(delRes.status).toBe(200);
expect(delRes.body.success).toBe(true);
const listRes = await request(app)
.get('/api/auth/mcp-tokens')
.set('Cookie', authCookie(user.id));
const listRes = await request(app).get('/api/auth/mcp-tokens').set('Cookie', authCookie(user.id));
expect(listRes.body.tokens).toEqual([]);
});
it('AUTH-038 — DELETE /auth/mcp-tokens/:id returns 404 for non-existent', async () => {
const { user } = createUser(testDb);
const res = await request(app)
.delete('/api/auth/mcp-tokens/99999')
.set('Cookie', authCookie(user.id));
const res = await request(app).delete('/api/auth/mcp-tokens/99999').set('Cookie', authCookie(user.id));
expect(res.status).toBe(404);
});