mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 22:31:46 +00:00
chore: apply prettier on the entire project
This commit is contained in:
@@ -4,17 +4,13 @@
|
||||
* Provides utilities to generate JWTs and authenticate supertest requests
|
||||
* using the fixed test JWT_SECRET from TEST_CONFIG.
|
||||
*/
|
||||
import { TEST_CONFIG } from './test-db';
|
||||
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { TEST_CONFIG } from './test-db';
|
||||
|
||||
/** Signs a JWT for the given user ID using the test secret. */
|
||||
export function generateToken(userId: number, extraClaims: Record<string, unknown> = {}): string {
|
||||
return jwt.sign(
|
||||
{ id: userId, ...extraClaims },
|
||||
TEST_CONFIG.JWT_SECRET,
|
||||
{ algorithm: 'HS256', expiresIn: '1h' }
|
||||
);
|
||||
return jwt.sign({ id: userId, ...extraClaims }, TEST_CONFIG.JWT_SECRET, { algorithm: 'HS256', expiresIn: '1h' });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+227
-183
@@ -3,11 +3,16 @@
|
||||
* Each factory inserts a row into the provided in-memory DB and returns the created object.
|
||||
* Passwords are stored as bcrypt hashes (cost factor 4 for speed in tests).
|
||||
*/
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { encryptMfaSecret } from '../../src/services/mfaCrypto';
|
||||
import { encrypt_api_key } from '../../src/services/apiKeyCrypto';
|
||||
import { encryptMfaSecret } from '../../src/services/mfaCrypto';
|
||||
|
||||
import bcrypt from 'bcryptjs';
|
||||
import Database from 'better-sqlite3';
|
||||
// ---------------------------------------------------------------------------
|
||||
// MCP Tokens
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
let _userSeq = 0;
|
||||
let _tripSeq = 0;
|
||||
@@ -28,7 +33,7 @@ export interface TestUser {
|
||||
|
||||
export function createUser(
|
||||
db: Database.Database,
|
||||
overrides: Partial<{ username: string; email: string; password: string; role: 'admin' | 'user' }> = {}
|
||||
overrides: Partial<{ username: string; email: string; password: string; role: 'admin' | 'user' }> = {},
|
||||
): { user: TestUser; password: string } {
|
||||
_userSeq++;
|
||||
const password = overrides.password ?? `TestPass${_userSeq}!`;
|
||||
@@ -37,9 +42,9 @@ export function createUser(
|
||||
const role = overrides.role ?? 'user';
|
||||
const hash = bcrypt.hashSync(password, 4); // cost 4 for test speed
|
||||
|
||||
const result = db.prepare(
|
||||
'INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)'
|
||||
).run(username, email, hash, role);
|
||||
const result = db
|
||||
.prepare('INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)')
|
||||
.run(username, email, hash, role);
|
||||
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(result.lastInsertRowid) as TestUser;
|
||||
return { user, password };
|
||||
@@ -47,7 +52,7 @@ export function createUser(
|
||||
|
||||
export function createAdmin(
|
||||
db: Database.Database,
|
||||
overrides: Partial<{ username: string; email: string; password: string }> = {}
|
||||
overrides: Partial<{ username: string; email: string; password: string }> = {},
|
||||
): { user: TestUser; password: string } {
|
||||
return createUser(db, { ...overrides, role: 'admin' });
|
||||
}
|
||||
@@ -59,13 +64,11 @@ export function createAdmin(
|
||||
const KNOWN_MFA_SECRET = 'JBSWY3DPEHPK3PXP'; // fixed base32 secret for deterministic tests
|
||||
export function createUserWithMfa(
|
||||
db: Database.Database,
|
||||
overrides: Partial<{ username: string; email: string; password: string; role: 'admin' | 'user' }> = {}
|
||||
overrides: Partial<{ username: string; email: string; password: string; role: 'admin' | 'user' }> = {},
|
||||
): { user: TestUser; password: string; totpSecret: string } {
|
||||
const { user, password } = createUser(db, overrides);
|
||||
const encryptedSecret = encryptMfaSecret(KNOWN_MFA_SECRET);
|
||||
db.prepare(
|
||||
'UPDATE users SET mfa_enabled = 1, mfa_secret = ? WHERE id = ?'
|
||||
).run(encryptedSecret, user.id);
|
||||
db.prepare('UPDATE users SET mfa_enabled = 1, mfa_secret = ? WHERE id = ?').run(encryptedSecret, user.id);
|
||||
const updated = db.prepare('SELECT * FROM users WHERE id = ?').get(user.id) as TestUser;
|
||||
return { user: updated, password, totpSecret: KNOWN_MFA_SECRET };
|
||||
}
|
||||
@@ -85,13 +88,13 @@ export interface TestTrip {
|
||||
export function createTrip(
|
||||
db: Database.Database,
|
||||
userId: number,
|
||||
overrides: Partial<{ title: string; start_date: string; end_date: string; description: string }> = {}
|
||||
overrides: Partial<{ title: string; start_date: string; end_date: string; description: string }> = {},
|
||||
): TestTrip {
|
||||
_tripSeq++;
|
||||
const title = overrides.title ?? `Test Trip ${_tripSeq}`;
|
||||
const result = db.prepare(
|
||||
'INSERT INTO trips (user_id, title, description, start_date, end_date) VALUES (?, ?, ?, ?, ?)'
|
||||
).run(userId, title, overrides.description ?? null, overrides.start_date ?? null, overrides.end_date ?? null);
|
||||
const result = db
|
||||
.prepare('INSERT INTO trips (user_id, title, description, start_date, end_date) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(userId, title, overrides.description ?? null, overrides.start_date ?? null, overrides.end_date ?? null);
|
||||
|
||||
// Auto-generate days if dates are provided
|
||||
if (overrides.start_date && overrides.end_date) {
|
||||
@@ -123,14 +126,16 @@ export interface TestDay {
|
||||
export function createDay(
|
||||
db: Database.Database,
|
||||
tripId: number,
|
||||
overrides: Partial<{ date: string; title: string; day_number: number }> = {}
|
||||
overrides: Partial<{ date: string; title: string; day_number: number }> = {},
|
||||
): TestDay {
|
||||
// Find the next day_number for this trip if not provided
|
||||
const maxDay = db.prepare('SELECT MAX(day_number) as max FROM days WHERE trip_id = ?').get(tripId) as { max: number | null };
|
||||
const maxDay = db.prepare('SELECT MAX(day_number) as max FROM days WHERE trip_id = ?').get(tripId) as {
|
||||
max: number | null;
|
||||
};
|
||||
const dayNumber = overrides.day_number ?? (maxDay.max ?? 0) + 1;
|
||||
const result = db.prepare(
|
||||
'INSERT INTO days (trip_id, day_number, date, title) VALUES (?, ?, ?, ?)'
|
||||
).run(tripId, dayNumber, overrides.date ?? null, overrides.title ?? null);
|
||||
const result = db
|
||||
.prepare('INSERT INTO days (trip_id, day_number, date, title) VALUES (?, ?, ?, ?)')
|
||||
.run(tripId, dayNumber, overrides.date ?? null, overrides.title ?? null);
|
||||
return db.prepare('SELECT * FROM days WHERE id = ?').get(result.lastInsertRowid) as TestDay;
|
||||
}
|
||||
|
||||
@@ -150,22 +155,22 @@ export interface TestPlace {
|
||||
export function createPlace(
|
||||
db: Database.Database,
|
||||
tripId: number,
|
||||
overrides: Partial<{ name: string; lat: number; lng: number; category_id: number; description: string }> = {}
|
||||
overrides: Partial<{ name: string; lat: number; lng: number; category_id: number; description: string }> = {},
|
||||
): TestPlace {
|
||||
// Get first available category if none provided
|
||||
const defaultCat = db.prepare('SELECT id FROM categories LIMIT 1').get() as { id: number } | undefined;
|
||||
const categoryId = overrides.category_id ?? defaultCat?.id ?? null;
|
||||
|
||||
const result = db.prepare(
|
||||
'INSERT INTO places (trip_id, name, lat, lng, category_id, description) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
).run(
|
||||
tripId,
|
||||
overrides.name ?? 'Test Place',
|
||||
overrides.lat ?? 48.8566,
|
||||
overrides.lng ?? 2.3522,
|
||||
categoryId,
|
||||
overrides.description ?? null
|
||||
);
|
||||
const result = db
|
||||
.prepare('INSERT INTO places (trip_id, name, lat, lng, category_id, description) VALUES (?, ?, ?, ?, ?, ?)')
|
||||
.run(
|
||||
tripId,
|
||||
overrides.name ?? 'Test Place',
|
||||
overrides.lat ?? 48.8566,
|
||||
overrides.lng ?? 2.3522,
|
||||
categoryId,
|
||||
overrides.description ?? null,
|
||||
);
|
||||
return db.prepare('SELECT * FROM places WHERE id = ?').get(result.lastInsertRowid) as TestPlace;
|
||||
}
|
||||
|
||||
@@ -192,16 +197,11 @@ export interface TestBudgetItem {
|
||||
export function createBudgetItem(
|
||||
db: Database.Database,
|
||||
tripId: number,
|
||||
overrides: Partial<{ name: string; category: string; total_price: number }> = {}
|
||||
overrides: Partial<{ name: string; category: string; total_price: number }> = {},
|
||||
): TestBudgetItem {
|
||||
const result = db.prepare(
|
||||
'INSERT INTO budget_items (trip_id, name, category, total_price) VALUES (?, ?, ?, ?)'
|
||||
).run(
|
||||
tripId,
|
||||
overrides.name ?? 'Test Budget Item',
|
||||
overrides.category ?? 'Transport',
|
||||
overrides.total_price ?? 100
|
||||
);
|
||||
const result = db
|
||||
.prepare('INSERT INTO budget_items (trip_id, name, category, total_price) VALUES (?, ?, ?, ?)')
|
||||
.run(tripId, overrides.name ?? 'Test Budget Item', overrides.category ?? 'Transport', overrides.total_price ?? 100);
|
||||
return db.prepare('SELECT * FROM budget_items WHERE id = ?').get(result.lastInsertRowid) as TestBudgetItem;
|
||||
}
|
||||
|
||||
@@ -220,11 +220,11 @@ export interface TestPackingItem {
|
||||
export function createPackingItem(
|
||||
db: Database.Database,
|
||||
tripId: number,
|
||||
overrides: Partial<{ name: string; category: string }> = {}
|
||||
overrides: Partial<{ name: string; category: string }> = {},
|
||||
): TestPackingItem {
|
||||
const result = db.prepare(
|
||||
'INSERT INTO packing_items (trip_id, name, category, checked) VALUES (?, ?, ?, 0)'
|
||||
).run(tripId, overrides.name ?? 'Test Item', overrides.category ?? 'Clothing');
|
||||
const result = db
|
||||
.prepare('INSERT INTO packing_items (trip_id, name, category, checked) VALUES (?, ?, ?, 0)')
|
||||
.run(tripId, overrides.name ?? 'Test Item', overrides.category ?? 'Clothing');
|
||||
return db.prepare('SELECT * FROM packing_items WHERE id = ?').get(result.lastInsertRowid) as TestPackingItem;
|
||||
}
|
||||
|
||||
@@ -242,11 +242,11 @@ export interface TestReservation {
|
||||
export function createReservation(
|
||||
db: Database.Database,
|
||||
tripId: number,
|
||||
overrides: Partial<{ title: string; type: string; day_id: number }> = {}
|
||||
overrides: Partial<{ title: string; type: string; day_id: number }> = {},
|
||||
): TestReservation {
|
||||
const result = db.prepare(
|
||||
'INSERT INTO reservations (trip_id, title, type, day_id) VALUES (?, ?, ?, ?)'
|
||||
).run(tripId, overrides.title ?? 'Test Reservation', overrides.type ?? 'flight', overrides.day_id ?? null);
|
||||
const result = db
|
||||
.prepare('INSERT INTO reservations (trip_id, title, type, day_id) VALUES (?, ?, ?, ?)')
|
||||
.run(tripId, overrides.title ?? 'Test Reservation', overrides.type ?? 'flight', overrides.day_id ?? null);
|
||||
return db.prepare('SELECT * FROM reservations WHERE id = ?').get(result.lastInsertRowid) as TestReservation;
|
||||
}
|
||||
|
||||
@@ -279,11 +279,11 @@ export function createDayNote(
|
||||
db: Database.Database,
|
||||
dayId: number,
|
||||
tripId: number,
|
||||
overrides: Partial<{ text: string; time: string; icon: string }> = {}
|
||||
overrides: Partial<{ text: string; time: string; icon: string }> = {},
|
||||
): TestDayNote {
|
||||
const result = db.prepare(
|
||||
'INSERT INTO day_notes (day_id, trip_id, text, time, icon, sort_order) VALUES (?, ?, ?, ?, ?, 9999)'
|
||||
).run(dayId, tripId, overrides.text ?? 'Test note', overrides.time ?? null, overrides.icon ?? '📝');
|
||||
const result = db
|
||||
.prepare('INSERT INTO day_notes (day_id, trip_id, text, time, icon, sort_order) VALUES (?, ?, ?, ?, ?, 9999)')
|
||||
.run(dayId, tripId, overrides.text ?? 'Test note', overrides.time ?? null, overrides.icon ?? '📝');
|
||||
return db.prepare('SELECT * FROM day_notes WHERE id = ?').get(result.lastInsertRowid) as TestDayNote;
|
||||
}
|
||||
|
||||
@@ -306,18 +306,18 @@ export function createCollabNote(
|
||||
db: Database.Database,
|
||||
tripId: number,
|
||||
userId: number,
|
||||
overrides: Partial<{ title: string; content: string; category: string; color: string }> = {}
|
||||
overrides: Partial<{ title: string; content: string; category: string; color: string }> = {},
|
||||
): TestCollabNote {
|
||||
const result = db.prepare(
|
||||
'INSERT INTO collab_notes (trip_id, user_id, title, content, category, color) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
).run(
|
||||
tripId,
|
||||
userId,
|
||||
overrides.title ?? 'Test Note',
|
||||
overrides.content ?? null,
|
||||
overrides.category ?? 'General',
|
||||
overrides.color ?? '#6366f1'
|
||||
);
|
||||
const result = db
|
||||
.prepare('INSERT INTO collab_notes (trip_id, user_id, title, content, category, color) VALUES (?, ?, ?, ?, ?, ?)')
|
||||
.run(
|
||||
tripId,
|
||||
userId,
|
||||
overrides.title ?? 'Test Note',
|
||||
overrides.content ?? null,
|
||||
overrides.category ?? 'General',
|
||||
overrides.color ?? '#6366f1',
|
||||
);
|
||||
return db.prepare('SELECT * FROM collab_notes WHERE id = ?').get(result.lastInsertRowid) as TestCollabNote;
|
||||
}
|
||||
|
||||
@@ -337,13 +337,15 @@ export interface TestTodoItem {
|
||||
export function createTodoItem(
|
||||
db: Database.Database,
|
||||
tripId: number,
|
||||
overrides: Partial<{ name: string; category: string; checked: number }> = {}
|
||||
overrides: Partial<{ name: string; category: string; checked: number }> = {},
|
||||
): TestTodoItem {
|
||||
const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM todo_items WHERE trip_id = ?').get(tripId) as { max: number | null };
|
||||
const maxOrder = db.prepare('SELECT MAX(sort_order) as max FROM todo_items WHERE trip_id = ?').get(tripId) as {
|
||||
max: number | null;
|
||||
};
|
||||
const sortOrder = (maxOrder.max !== null ? maxOrder.max : -1) + 1;
|
||||
const result = db.prepare(
|
||||
'INSERT INTO todo_items (trip_id, name, checked, category, sort_order) VALUES (?, ?, ?, ?, ?)'
|
||||
).run(tripId, overrides.name ?? 'Test Todo', overrides.checked ?? 0, overrides.category ?? null, sortOrder);
|
||||
const result = db
|
||||
.prepare('INSERT INTO todo_items (trip_id, name, checked, category, sort_order) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(tripId, overrides.name ?? 'Test Todo', overrides.checked ?? 0, overrides.category ?? null, sortOrder);
|
||||
return db.prepare('SELECT * FROM todo_items WHERE id = ?').get(result.lastInsertRowid) as TestTodoItem;
|
||||
}
|
||||
|
||||
@@ -363,13 +365,15 @@ export function createDayAssignment(
|
||||
db: Database.Database,
|
||||
dayId: number,
|
||||
placeId: number,
|
||||
overrides: Partial<{ order_index: number; notes: string }> = {}
|
||||
overrides: Partial<{ order_index: number; notes: string }> = {},
|
||||
): TestDayAssignment {
|
||||
const maxOrder = db.prepare('SELECT MAX(order_index) as max FROM day_assignments WHERE day_id = ?').get(dayId) as { max: number | null };
|
||||
const maxOrder = db.prepare('SELECT MAX(order_index) as max FROM day_assignments WHERE day_id = ?').get(dayId) as {
|
||||
max: number | null;
|
||||
};
|
||||
const orderIndex = overrides.order_index ?? (maxOrder.max !== null ? maxOrder.max + 1 : 0);
|
||||
const result = db.prepare(
|
||||
'INSERT INTO day_assignments (day_id, place_id, order_index, notes) VALUES (?, ?, ?, ?)'
|
||||
).run(dayId, placeId, orderIndex, overrides.notes ?? null);
|
||||
const result = db
|
||||
.prepare('INSERT INTO day_assignments (day_id, place_id, order_index, notes) VALUES (?, ?, ?, ?)')
|
||||
.run(dayId, placeId, orderIndex, overrides.notes ?? null);
|
||||
return db.prepare('SELECT * FROM day_assignments WHERE id = ?').get(result.lastInsertRowid) as TestDayAssignment;
|
||||
}
|
||||
|
||||
@@ -390,18 +394,18 @@ export interface TestBucketListItem {
|
||||
export function createBucketListItem(
|
||||
db: Database.Database,
|
||||
userId: number,
|
||||
overrides: Partial<{ name: string; lat: number; lng: number; country_code: string; notes: string }> = {}
|
||||
overrides: Partial<{ name: string; lat: number; lng: number; country_code: string; notes: string }> = {},
|
||||
): TestBucketListItem {
|
||||
const result = db.prepare(
|
||||
'INSERT INTO bucket_list (user_id, name, lat, lng, country_code, notes) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
).run(
|
||||
userId,
|
||||
overrides.name ?? 'Test Destination',
|
||||
overrides.lat ?? null,
|
||||
overrides.lng ?? null,
|
||||
overrides.country_code ?? null,
|
||||
overrides.notes ?? null
|
||||
);
|
||||
const result = db
|
||||
.prepare('INSERT INTO bucket_list (user_id, name, lat, lng, country_code, notes) VALUES (?, ?, ?, ?, ?, ?)')
|
||||
.run(
|
||||
userId,
|
||||
overrides.name ?? 'Test Destination',
|
||||
overrides.lat ?? null,
|
||||
overrides.lng ?? null,
|
||||
overrides.country_code ?? null,
|
||||
overrides.notes ?? null,
|
||||
);
|
||||
return db.prepare('SELECT * FROM bucket_list WHERE id = ?').get(result.lastInsertRowid) as TestBucketListItem;
|
||||
}
|
||||
|
||||
@@ -409,12 +413,11 @@ export function createBucketListItem(
|
||||
// Visited Countries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function createVisitedCountry(
|
||||
db: Database.Database,
|
||||
userId: number,
|
||||
countryCode: string
|
||||
): void {
|
||||
db.prepare('INSERT OR IGNORE INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(userId, countryCode.toUpperCase());
|
||||
export function createVisitedCountry(db: Database.Database, userId: number, countryCode: string): void {
|
||||
db.prepare('INSERT OR IGNORE INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(
|
||||
userId,
|
||||
countryCode.toUpperCase(),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -437,28 +440,26 @@ export function createDayAccommodation(
|
||||
placeId: number,
|
||||
startDayId: number,
|
||||
endDayId: number,
|
||||
overrides: Partial<{ check_in: string; check_out: string; confirmation: string }> = {}
|
||||
overrides: Partial<{ check_in: string; check_out: string; confirmation: string }> = {},
|
||||
): TestDayAccommodation {
|
||||
const result = db.prepare(
|
||||
'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_out, confirmation) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
).run(
|
||||
tripId,
|
||||
placeId,
|
||||
startDayId,
|
||||
endDayId,
|
||||
overrides.check_in ?? null,
|
||||
overrides.check_out ?? null,
|
||||
overrides.confirmation ?? null
|
||||
);
|
||||
return db.prepare('SELECT * FROM day_accommodations WHERE id = ?').get(result.lastInsertRowid) as TestDayAccommodation;
|
||||
const result = db
|
||||
.prepare(
|
||||
'INSERT INTO day_accommodations (trip_id, place_id, start_day_id, end_day_id, check_in, check_out, confirmation) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
)
|
||||
.run(
|
||||
tripId,
|
||||
placeId,
|
||||
startDayId,
|
||||
endDayId,
|
||||
overrides.check_in ?? null,
|
||||
overrides.check_out ?? null,
|
||||
overrides.confirmation ?? null,
|
||||
);
|
||||
return db
|
||||
.prepare('SELECT * FROM day_accommodations WHERE id = ?')
|
||||
.get(result.lastInsertRowid) as TestDayAccommodation;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MCP Tokens
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
|
||||
export interface TestMcpToken {
|
||||
id: number;
|
||||
tokenHash: string;
|
||||
@@ -468,14 +469,14 @@ export interface TestMcpToken {
|
||||
export function createMcpToken(
|
||||
db: Database.Database,
|
||||
userId: number,
|
||||
overrides: Partial<{ name: string; rawToken: string }> = {}
|
||||
overrides: Partial<{ name: string; rawToken: string }> = {},
|
||||
): TestMcpToken {
|
||||
const rawToken = overrides.rawToken ?? `trek_test_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
const tokenHash = createHash('sha256').update(rawToken).digest('hex');
|
||||
const tokenPrefix = rawToken.slice(0, 12);
|
||||
const result = db.prepare(
|
||||
'INSERT INTO mcp_tokens (user_id, token_hash, token_prefix, name) VALUES (?, ?, ?, ?)'
|
||||
).run(userId, tokenHash, tokenPrefix, overrides.name ?? 'Test Token');
|
||||
const result = db
|
||||
.prepare('INSERT INTO mcp_tokens (user_id, token_hash, token_prefix, name) VALUES (?, ?, ?, ?)')
|
||||
.run(userId, tokenHash, tokenPrefix, overrides.name ?? 'Test Token');
|
||||
return { id: result.lastInsertRowid as number, tokenHash, rawToken };
|
||||
}
|
||||
|
||||
@@ -485,7 +486,7 @@ export function createMcpToken(
|
||||
|
||||
export function createInviteToken(
|
||||
db: Database.Database,
|
||||
overrides: Partial<{ token: string; max_uses: number; expires_at: string; created_by: number }> = {}
|
||||
overrides: Partial<{ token: string; max_uses: number; expires_at: string; created_by: number }> = {},
|
||||
): TestInviteToken {
|
||||
const token = overrides.token ?? `test-invite-${Date.now()}`;
|
||||
// created_by is required by the schema; use an existing admin or create one
|
||||
@@ -499,14 +500,18 @@ export function createInviteToken(
|
||||
if (any) {
|
||||
createdBy = any.id;
|
||||
} else {
|
||||
const r = db.prepare("INSERT INTO users (username, email, password_hash, role) VALUES ('invite_creator', 'invite_creator@test.example.com', 'x', 'admin')").run();
|
||||
const r = db
|
||||
.prepare(
|
||||
"INSERT INTO users (username, email, password_hash, role) VALUES ('invite_creator', 'invite_creator@test.example.com', 'x', 'admin')",
|
||||
)
|
||||
.run();
|
||||
createdBy = r.lastInsertRowid as number;
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = db.prepare(
|
||||
'INSERT INTO invite_tokens (token, max_uses, used_count, expires_at, created_by) VALUES (?, ?, 0, ?, ?)'
|
||||
).run(token, overrides.max_uses ?? 1, overrides.expires_at ?? null, createdBy);
|
||||
const result = db
|
||||
.prepare('INSERT INTO invite_tokens (token, max_uses, used_count, expires_at, created_by) VALUES (?, ?, 0, ?, ?)')
|
||||
.run(token, overrides.max_uses ?? 1, overrides.expires_at ?? null, createdBy);
|
||||
return db.prepare('SELECT * FROM invite_tokens WHERE id = ?').get(result.lastInsertRowid) as TestInviteToken;
|
||||
}
|
||||
|
||||
@@ -529,10 +534,10 @@ export function disableNotificationPref(
|
||||
db: Database.Database,
|
||||
userId: number,
|
||||
eventType: string,
|
||||
channel: string
|
||||
channel: string,
|
||||
): void {
|
||||
db.prepare(
|
||||
'INSERT OR REPLACE INTO notification_channel_preferences (user_id, event_type, channel, enabled) VALUES (?, ?, ?, 0)'
|
||||
'INSERT OR REPLACE INTO notification_channel_preferences (user_id, event_type, channel, enabled) VALUES (?, ?, ?, 0)',
|
||||
).run(userId, eventType, channel);
|
||||
}
|
||||
|
||||
@@ -556,25 +561,33 @@ export function addTripPhoto(
|
||||
userId: number,
|
||||
assetId: string,
|
||||
provider: string,
|
||||
opts: { shared?: boolean; albumLinkId?: number } = {}
|
||||
opts: { shared?: boolean; albumLinkId?: number } = {},
|
||||
): TestTripPhoto {
|
||||
// Insert into trek_photos first (central registry)
|
||||
db.prepare(
|
||||
'INSERT OR IGNORE INTO trek_photos (provider, asset_id, owner_id) VALUES (?, ?, ?)'
|
||||
).run(provider, assetId, userId);
|
||||
const trekPhoto = db.prepare(
|
||||
'SELECT id FROM trek_photos WHERE provider = ? AND asset_id = ? AND owner_id = ?'
|
||||
).get(provider, assetId, userId) as { id: number };
|
||||
db.prepare('INSERT OR IGNORE INTO trek_photos (provider, asset_id, owner_id) VALUES (?, ?, ?)').run(
|
||||
provider,
|
||||
assetId,
|
||||
userId,
|
||||
);
|
||||
const trekPhoto = db
|
||||
.prepare('SELECT id FROM trek_photos WHERE provider = ? AND asset_id = ? AND owner_id = ?')
|
||||
.get(provider, assetId, userId) as { id: number };
|
||||
|
||||
const result = db.prepare(
|
||||
'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, photo_id, shared, album_link_id) VALUES (?, ?, ?, ?, ?)'
|
||||
).run(tripId, userId, trekPhoto.id, opts.shared ? 1 : 0, opts.albumLinkId ?? null);
|
||||
return db.prepare(`
|
||||
const result = db
|
||||
.prepare(
|
||||
'INSERT OR IGNORE INTO trip_photos (trip_id, user_id, photo_id, shared, album_link_id) VALUES (?, ?, ?, ?, ?)',
|
||||
)
|
||||
.run(tripId, userId, trekPhoto.id, opts.shared ? 1 : 0, opts.albumLinkId ?? null);
|
||||
return db
|
||||
.prepare(
|
||||
`
|
||||
SELECT tp.id, tp.trip_id, tp.user_id, tkp.asset_id, tkp.provider, tp.shared, tp.album_link_id
|
||||
FROM trip_photos tp
|
||||
JOIN trek_photos tkp ON tkp.id = tp.photo_id
|
||||
WHERE tp.id = ?
|
||||
`).get(result.lastInsertRowid) as TestTripPhoto;
|
||||
`,
|
||||
)
|
||||
.get(result.lastInsertRowid) as TestTripPhoto;
|
||||
}
|
||||
|
||||
export interface TestAlbumLink {
|
||||
@@ -592,22 +605,20 @@ export function addAlbumLink(
|
||||
userId: number,
|
||||
provider: string,
|
||||
albumId: string,
|
||||
albumName = 'Test Album'
|
||||
albumName = 'Test Album',
|
||||
): TestAlbumLink {
|
||||
const result = db.prepare(
|
||||
'INSERT INTO trip_album_links (trip_id, user_id, provider, album_id, album_name) VALUES (?, ?, ?, ?, ?)'
|
||||
).run(tripId, userId, provider, albumId, albumName);
|
||||
const result = db
|
||||
.prepare('INSERT INTO trip_album_links (trip_id, user_id, provider, album_id, album_name) VALUES (?, ?, ?, ?, ?)')
|
||||
.run(tripId, userId, provider, albumId, albumName);
|
||||
return db.prepare('SELECT * FROM trip_album_links WHERE id = ?').get(result.lastInsertRowid) as TestAlbumLink;
|
||||
}
|
||||
|
||||
export function setImmichCredentials(
|
||||
db: Database.Database,
|
||||
userId: number,
|
||||
url: string,
|
||||
apiKey: string
|
||||
): void {
|
||||
db.prepare('UPDATE users SET immich_url = ?, immich_api_key = ? WHERE id = ?')
|
||||
.run(url, encrypt_api_key(apiKey), userId);
|
||||
export function setImmichCredentials(db: Database.Database, userId: number, url: string, apiKey: string): void {
|
||||
db.prepare('UPDATE users SET immich_url = ?, immich_api_key = ? WHERE id = ?').run(
|
||||
url,
|
||||
encrypt_api_key(apiKey),
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
export function setSynologyCredentials(
|
||||
@@ -615,10 +626,14 @@ export function setSynologyCredentials(
|
||||
userId: number,
|
||||
url: string,
|
||||
username: string,
|
||||
password: string
|
||||
password: string,
|
||||
): void {
|
||||
db.prepare('UPDATE users SET synology_url = ?, synology_username = ?, synology_password = ? WHERE id = ?')
|
||||
.run(url, username, encrypt_api_key(password), userId);
|
||||
db.prepare('UPDATE users SET synology_url = ?, synology_username = ?, synology_password = ? WHERE id = ?').run(
|
||||
url,
|
||||
username,
|
||||
encrypt_api_key(password),
|
||||
userId,
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -627,29 +642,38 @@ export function setSynologyCredentials(
|
||||
|
||||
export function createCategory(
|
||||
db: Database.Database,
|
||||
overrides: { name?: string; color?: string; icon?: string; user_id?: number | null } = {}
|
||||
overrides: { name?: string; color?: string; icon?: string; user_id?: number | null } = {},
|
||||
) {
|
||||
const name = overrides.name ?? `Test Category ${++_categorySeq}`;
|
||||
const color = overrides.color ?? '#6366f1';
|
||||
const icon = overrides.icon ?? '📍';
|
||||
const userId = overrides.user_id ?? null;
|
||||
const result = db.prepare('INSERT INTO categories (name, color, icon, user_id) VALUES (?, ?, ?, ?)').run(name, color, icon, userId);
|
||||
return db.prepare('SELECT * FROM categories WHERE id = ?').get(result.lastInsertRowid) as { id: number; name: string; color: string; icon: string; user_id: number | null };
|
||||
const result = db
|
||||
.prepare('INSERT INTO categories (name, color, icon, user_id) VALUES (?, ?, ?, ?)')
|
||||
.run(name, color, icon, userId);
|
||||
return db.prepare('SELECT * FROM categories WHERE id = ?').get(result.lastInsertRowid) as {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
user_id: number | null;
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tags
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function createTag(
|
||||
db: Database.Database,
|
||||
userId: number,
|
||||
overrides: { name?: string; color?: string } = {}
|
||||
) {
|
||||
export function createTag(db: Database.Database, userId: number, overrides: { name?: string; color?: string } = {}) {
|
||||
const name = overrides.name ?? `Test Tag ${++_tagSeq}`;
|
||||
const color = overrides.color ?? '#10b981';
|
||||
const result = db.prepare('INSERT INTO tags (user_id, name, color) VALUES (?, ?, ?)').run(userId, name, color);
|
||||
return db.prepare('SELECT * FROM tags WHERE id = ?').get(result.lastInsertRowid) as { id: number; user_id: number; name: string; color: string };
|
||||
return db.prepare('SELECT * FROM tags WHERE id = ?').get(result.lastInsertRowid) as {
|
||||
id: number;
|
||||
user_id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -672,21 +696,25 @@ export interface TestJourney {
|
||||
export function createJourney(
|
||||
db: Database.Database,
|
||||
userId: number,
|
||||
overrides: Partial<{ title: string; subtitle: string; status: string }> = {}
|
||||
overrides: Partial<{ title: string; subtitle: string; status: string }> = {},
|
||||
): TestJourney {
|
||||
_journeySeq++;
|
||||
const title = overrides.title ?? `Test Journey ${_journeySeq}`;
|
||||
const now = Date.now();
|
||||
const result = db.prepare(
|
||||
'INSERT INTO journeys (user_id, title, subtitle, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
).run(userId, title, overrides.subtitle ?? null, overrides.status ?? 'active', now, now);
|
||||
const result = db
|
||||
.prepare(
|
||||
'INSERT INTO journeys (user_id, title, subtitle, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
)
|
||||
.run(userId, title, overrides.subtitle ?? null, overrides.status ?? 'active', now, now);
|
||||
|
||||
const journeyId = result.lastInsertRowid as number;
|
||||
|
||||
// Auto-add owner as contributor
|
||||
db.prepare(
|
||||
"INSERT INTO journey_contributors (journey_id, user_id, role, added_at) VALUES (?, ?, 'owner', ?)"
|
||||
).run(journeyId, userId, now);
|
||||
db.prepare("INSERT INTO journey_contributors (journey_id, user_id, role, added_at) VALUES (?, ?, 'owner', ?)").run(
|
||||
journeyId,
|
||||
userId,
|
||||
now,
|
||||
);
|
||||
|
||||
return db.prepare('SELECT * FROM journeys WHERE id = ?').get(journeyId) as TestJourney;
|
||||
}
|
||||
@@ -705,23 +733,37 @@ export function createJourneyEntry(
|
||||
db: Database.Database,
|
||||
journeyId: number,
|
||||
authorId: number,
|
||||
overrides: Partial<{ type: string; entry_date: string; title: string; story: string; location_name: string; mood: string; weather: string }> = {}
|
||||
overrides: Partial<{
|
||||
type: string;
|
||||
entry_date: string;
|
||||
title: string;
|
||||
story: string;
|
||||
location_name: string;
|
||||
mood: string;
|
||||
weather: string;
|
||||
}> = {},
|
||||
): TestJourneyEntry {
|
||||
const now = Date.now();
|
||||
const result = db.prepare(`
|
||||
const result = db
|
||||
.prepare(
|
||||
`
|
||||
INSERT INTO journey_entries (journey_id, author_id, type, entry_date, title, story, location_name, mood, weather, visibility, sort_order, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'private', 0, ?, ?)
|
||||
`).run(
|
||||
journeyId, authorId,
|
||||
overrides.type ?? 'entry',
|
||||
overrides.entry_date ?? '2026-01-15',
|
||||
overrides.title ?? null,
|
||||
overrides.story ?? null,
|
||||
overrides.location_name ?? null,
|
||||
overrides.mood ?? null,
|
||||
overrides.weather ?? null,
|
||||
now, now
|
||||
);
|
||||
`,
|
||||
)
|
||||
.run(
|
||||
journeyId,
|
||||
authorId,
|
||||
overrides.type ?? 'entry',
|
||||
overrides.entry_date ?? '2026-01-15',
|
||||
overrides.title ?? null,
|
||||
overrides.story ?? null,
|
||||
overrides.location_name ?? null,
|
||||
overrides.mood ?? null,
|
||||
overrides.weather ?? null,
|
||||
now,
|
||||
now,
|
||||
);
|
||||
return db.prepare('SELECT * FROM journey_entries WHERE id = ?').get(result.lastInsertRowid) as TestJourneyEntry;
|
||||
}
|
||||
|
||||
@@ -729,15 +771,17 @@ export function addJourneyContributor(
|
||||
db: Database.Database,
|
||||
journeyId: number,
|
||||
userId: number,
|
||||
role: 'editor' | 'viewer' = 'editor'
|
||||
role: 'editor' | 'viewer' = 'editor',
|
||||
): void {
|
||||
db.prepare(
|
||||
'INSERT OR IGNORE INTO journey_contributors (journey_id, user_id, role, added_at) VALUES (?, ?, ?, ?)'
|
||||
'INSERT OR IGNORE INTO journey_contributors (journey_id, user_id, role, added_at) VALUES (?, ?, ?, ?)',
|
||||
).run(journeyId, userId, role, Date.now());
|
||||
}
|
||||
|
||||
export function linkTripToJourney(db: Database.Database, journeyId: number, tripId: number): void {
|
||||
db.prepare(
|
||||
'INSERT OR IGNORE INTO journey_trips (journey_id, trip_id, linked_at) VALUES (?, ?, ?)'
|
||||
).run(journeyId, tripId, Date.now());
|
||||
db.prepare('INSERT OR IGNORE INTO journey_trips (journey_id, trip_id, linked_at) VALUES (?, ?, ?)').run(
|
||||
journeyId,
|
||||
tripId,
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,12 +9,11 @@
|
||||
* const result = await harness.client.callTool({ name: 'create_trip', arguments: { title: 'Test' } });
|
||||
* await harness.cleanup();
|
||||
*/
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index';
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory';
|
||||
import { registerResources } from '../../src/mcp/resources';
|
||||
import { registerTools } from '../../src/mcp/tools';
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index';
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory';
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
||||
|
||||
export interface McpHarness {
|
||||
client: Client;
|
||||
@@ -50,8 +49,16 @@ export async function createMcpHarness(options: McpHarnessOptions): Promise<McpH
|
||||
await client.connect(clientTransport);
|
||||
|
||||
const cleanup = async () => {
|
||||
try { await client.close(); } catch { /* ignore */ }
|
||||
try { await server.close(); } catch { /* ignore */ }
|
||||
try {
|
||||
await client.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
await server.close();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
return { client, server, cleanup };
|
||||
@@ -59,7 +66,9 @@ export async function createMcpHarness(options: McpHarnessOptions): Promise<McpH
|
||||
|
||||
/** Parse JSON from a callTool result (first text content item). */
|
||||
export function parseToolResult(result: Awaited<ReturnType<Client['callTool']>>): unknown {
|
||||
const text = result.content.find((c: { type: string }) => c.type === 'text') as { type: 'text'; text: string } | undefined;
|
||||
const text = result.content.find((c: { type: string }) => c.type === 'text') as
|
||||
| { type: 'text'; text: string }
|
||||
| undefined;
|
||||
if (!text) throw new Error('No text content in tool result');
|
||||
return JSON.parse(text.text);
|
||||
}
|
||||
|
||||
+112
-28
@@ -15,10 +15,10 @@
|
||||
* beforeEach(() => resetTestDb(testDb));
|
||||
* afterAll(() => testDb.close());
|
||||
*/
|
||||
import { runMigrations } from '../../src/db/migrations';
|
||||
import { createTables } from '../../src/db/schema';
|
||||
|
||||
import Database from 'better-sqlite3';
|
||||
import { createTables } from '../../src/db/schema';
|
||||
import { runMigrations } from '../../src/db/migrations';
|
||||
|
||||
// Tables to clear on reset, child-before-parent to be safe (FK checks are OFF during reset).
|
||||
// Keep in sync with schema.ts + migrations.ts. Intentionally excluded: categories, addons,
|
||||
@@ -116,32 +116,102 @@ const DEFAULT_CATEGORIES = [
|
||||
];
|
||||
|
||||
const DEFAULT_ADDONS = [
|
||||
{ id: 'packing', name: 'Packing List', description: 'Pack your bags', type: 'trip', icon: 'ListChecks', enabled: 1, sort_order: 0 },
|
||||
{ id: 'budget', name: 'Budget Planner', description: 'Track expenses', type: 'trip', icon: 'Wallet', enabled: 1, sort_order: 1 },
|
||||
{ id: 'documents', name: 'Documents', description: 'Manage travel documents', type: 'trip', icon: 'FileText', enabled: 1, sort_order: 2 },
|
||||
{ id: 'vacay', name: 'Vacay', description: 'Vacation day planner', type: 'global', icon: 'CalendarDays',enabled: 1, sort_order: 10 },
|
||||
{ id: 'atlas', name: 'Atlas', description: 'Visited countries map', type: 'global', icon: 'Globe', enabled: 1, sort_order: 11 },
|
||||
{ id: 'mcp', name: 'MCP', description: 'AI assistant integration', type: 'integration', icon: 'Terminal', enabled: 0, sort_order: 12 },
|
||||
{ id: 'naver_list_import', name: 'Naver List Import', description: 'Import places from shared Naver Maps lists', type: 'trip', icon: 'Link2', enabled: 0, sort_order: 13 },
|
||||
{ id: 'collab', name: 'Collab', description: 'Notes, polls, live chat', type: 'trip', icon: 'Users', enabled: 1, sort_order: 6 },
|
||||
{
|
||||
id: 'packing',
|
||||
name: 'Packing List',
|
||||
description: 'Pack your bags',
|
||||
type: 'trip',
|
||||
icon: 'ListChecks',
|
||||
enabled: 1,
|
||||
sort_order: 0,
|
||||
},
|
||||
{
|
||||
id: 'budget',
|
||||
name: 'Budget Planner',
|
||||
description: 'Track expenses',
|
||||
type: 'trip',
|
||||
icon: 'Wallet',
|
||||
enabled: 1,
|
||||
sort_order: 1,
|
||||
},
|
||||
{
|
||||
id: 'documents',
|
||||
name: 'Documents',
|
||||
description: 'Manage travel documents',
|
||||
type: 'trip',
|
||||
icon: 'FileText',
|
||||
enabled: 1,
|
||||
sort_order: 2,
|
||||
},
|
||||
{
|
||||
id: 'vacay',
|
||||
name: 'Vacay',
|
||||
description: 'Vacation day planner',
|
||||
type: 'global',
|
||||
icon: 'CalendarDays',
|
||||
enabled: 1,
|
||||
sort_order: 10,
|
||||
},
|
||||
{
|
||||
id: 'atlas',
|
||||
name: 'Atlas',
|
||||
description: 'Visited countries map',
|
||||
type: 'global',
|
||||
icon: 'Globe',
|
||||
enabled: 1,
|
||||
sort_order: 11,
|
||||
},
|
||||
{
|
||||
id: 'mcp',
|
||||
name: 'MCP',
|
||||
description: 'AI assistant integration',
|
||||
type: 'integration',
|
||||
icon: 'Terminal',
|
||||
enabled: 0,
|
||||
sort_order: 12,
|
||||
},
|
||||
{
|
||||
id: 'naver_list_import',
|
||||
name: 'Naver List Import',
|
||||
description: 'Import places from shared Naver Maps lists',
|
||||
type: 'trip',
|
||||
icon: 'Link2',
|
||||
enabled: 0,
|
||||
sort_order: 13,
|
||||
},
|
||||
{
|
||||
id: 'collab',
|
||||
name: 'Collab',
|
||||
description: 'Notes, polls, live chat',
|
||||
type: 'trip',
|
||||
icon: 'Users',
|
||||
enabled: 1,
|
||||
sort_order: 6,
|
||||
},
|
||||
];
|
||||
|
||||
const DEFAULT_PHOTO_PROVIDERS = [
|
||||
{ id: 'immich', name: 'Immich', enabled: 1 },
|
||||
{ id: 'synologyphotos', name: 'Synology Photos', enabled: 1 },
|
||||
{ id: 'immich', name: 'Immich', enabled: 1 },
|
||||
{ id: 'synologyphotos', name: 'Synology Photos', enabled: 1 },
|
||||
];
|
||||
|
||||
function seedDefaults(db: Database.Database): void {
|
||||
const insertCat = db.prepare('INSERT OR IGNORE INTO categories (name, color, icon) VALUES (?, ?, ?)');
|
||||
for (const cat of DEFAULT_CATEGORIES) insertCat.run(cat.name, cat.color, cat.icon);
|
||||
|
||||
const insertAddon = db.prepare('INSERT OR IGNORE INTO addons (id, name, description, type, icon, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)');
|
||||
const insertAddon = db.prepare(
|
||||
'INSERT OR IGNORE INTO addons (id, name, description, type, icon, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||||
);
|
||||
for (const a of DEFAULT_ADDONS) insertAddon.run(a.id, a.name, a.description, a.type, a.icon, a.enabled, a.sort_order);
|
||||
|
||||
try {
|
||||
const insertProvider = db.prepare('INSERT OR IGNORE INTO photo_providers (id, name, description, icon, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?)');
|
||||
const insertProvider = db.prepare(
|
||||
'INSERT OR IGNORE INTO photo_providers (id, name, description, icon, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
);
|
||||
for (const p of DEFAULT_PHOTO_PROVIDERS) insertProvider.run(p.id, p.name, p.id, 'Image', p.enabled, 0);
|
||||
} catch { /* table may not exist in very old schemas */ }
|
||||
} catch {
|
||||
/* table may not exist in very old schemas */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,7 +236,7 @@ export function createTestDb(): Database.Database {
|
||||
export function resetTestDb(db: Database.Database): void {
|
||||
db.exec('PRAGMA foreign_keys = OFF');
|
||||
const existingTables = new Set(
|
||||
(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[]).map(r => r.name)
|
||||
(db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[]).map((r) => r.name),
|
||||
);
|
||||
for (const table of RESET_TABLES) {
|
||||
if (existingTables.has(table)) {
|
||||
@@ -199,38 +269,52 @@ export function buildDbMock(testDb: Database.Database) {
|
||||
category_icon: string | null;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
const place = testDb.prepare(`
|
||||
const place = testDb
|
||||
.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) as PlaceRow | undefined;
|
||||
`,
|
||||
)
|
||||
.get(placeId) as PlaceRow | undefined;
|
||||
|
||||
if (!place) return null;
|
||||
|
||||
const tags = testDb.prepare(`
|
||||
const tags = testDb
|
||||
.prepare(
|
||||
`
|
||||
SELECT t.* FROM tags t
|
||||
JOIN place_tags pt ON t.id = pt.tag_id
|
||||
WHERE pt.place_id = ?
|
||||
`).all(placeId);
|
||||
`,
|
||||
)
|
||||
.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,
|
||||
category: place.category_id
|
||||
? {
|
||||
id: place.category_id,
|
||||
name: place.category_name,
|
||||
color: place.category_color,
|
||||
icon: place.category_icon,
|
||||
}
|
||||
: null,
|
||||
tags,
|
||||
};
|
||||
},
|
||||
canAccessTrip: (tripId: number | string, userId: number) => {
|
||||
return testDb.prepare(`
|
||||
return testDb
|
||||
.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);
|
||||
`,
|
||||
)
|
||||
.get(userId, tripId, userId);
|
||||
},
|
||||
isOwner: (tripId: number | string, userId: number) => {
|
||||
return !!testDb.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId);
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
* expect(msg.type).toBe('welcome');
|
||||
* });
|
||||
*/
|
||||
|
||||
import WebSocket from 'ws';
|
||||
|
||||
export interface WsMessage {
|
||||
@@ -44,35 +43,40 @@ export class WsTestClient {
|
||||
this.ws.on('message', (data: WebSocket.RawData) => {
|
||||
try {
|
||||
const msg = JSON.parse(data.toString()) as WsMessage;
|
||||
const waiterIdx = this.waiters.findIndex(w => w.type === msg.type || w.type === '*');
|
||||
const waiterIdx = this.waiters.findIndex((w) => w.type === msg.type || w.type === '*');
|
||||
if (waiterIdx >= 0) {
|
||||
const waiter = this.waiters.splice(waiterIdx, 1)[0];
|
||||
waiter.resolve(msg);
|
||||
} else {
|
||||
this.messageQueue.push(msg);
|
||||
}
|
||||
} catch { /* ignore malformed messages */ }
|
||||
} catch {
|
||||
/* ignore malformed messages */
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Wait for a message of the given type (or '*' for any). */
|
||||
waitForMessage(type: string, timeoutMs = 5000): Promise<WsMessage> {
|
||||
// Check if already in queue
|
||||
const idx = this.messageQueue.findIndex(m => type === '*' || m.type === type);
|
||||
const idx = this.messageQueue.findIndex((m) => type === '*' || m.type === type);
|
||||
if (idx >= 0) {
|
||||
return Promise.resolve(this.messageQueue.splice(idx, 1)[0]);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
const waiterIdx = this.waiters.findIndex(w => w.resolve === resolve);
|
||||
const waiterIdx = this.waiters.findIndex((w) => w.resolve === resolve);
|
||||
if (waiterIdx >= 0) this.waiters.splice(waiterIdx, 1);
|
||||
reject(new Error(`Timed out waiting for WS message type="${type}" after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
|
||||
this.waiters.push({
|
||||
type,
|
||||
resolve: (msg) => { clearTimeout(timer); resolve(msg); },
|
||||
resolve: (msg) => {
|
||||
clearTimeout(timer);
|
||||
resolve(msg);
|
||||
},
|
||||
reject,
|
||||
});
|
||||
});
|
||||
@@ -93,8 +97,14 @@ export class WsTestClient {
|
||||
if (this.ws.readyState === WebSocket.OPEN) return Promise.resolve();
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('WS open timed out')), timeoutMs);
|
||||
this.ws.once('open', () => { clearTimeout(timer); resolve(); });
|
||||
this.ws.once('error', (err) => { clearTimeout(timer); reject(err); });
|
||||
this.ws.once('open', () => {
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
});
|
||||
this.ws.once('error', (err) => {
|
||||
clearTimeout(timer);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -103,7 +113,10 @@ export class WsTestClient {
|
||||
if (this.ws.readyState === WebSocket.CLOSED) return Promise.resolve(1000);
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error('WS close timed out')), timeoutMs);
|
||||
this.ws.once('close', (code) => { clearTimeout(timer); resolve(code); });
|
||||
this.ws.once('close', (code) => {
|
||||
clearTimeout(timer);
|
||||
resolve(code);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user