mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
chore: apply prettier on the entire project
This commit is contained in:
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user