2026-04-03 19:17:53 +08:00
/ * *
* Test data factories .
* 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' ;
2026-04-06 03:11:43 +08:00
import { encrypt_api_key } from '../../src/services/apiKeyCrypto' ;
2026-04-03 19:17:53 +08:00
let _userSeq = 0 ;
let _tripSeq = 0 ;
test: expand test suite to 87.3% backend coverage
Add new integration test files covering previously untested routes:
- categories.test.ts — GET /api/categories
- oidc.test.ts — full OIDC login flow (callback, state, errors)
- settings.test.ts — GET/PUT /api/settings, bulk save
- tags.test.ts — CRUD for trip tags
- todo.test.ts — todo items CRUD and reorder
Add new unit test files covering service-layer logic:
- adminService.test.ts — user/invite management, packing templates, OIDC settings
- atlasService.test.ts — atlas search and place enrichment
- authServiceDb.test.ts — DB-backed auth helpers (login, register, MFA)
- backupService.test.ts — export/import/restore logic
- categoryService.test.ts — category CRUD
- dayService.test.ts — day management and accommodation helpers
- mapsService.test.ts — route/directions helpers
- oidcService.test.ts — OIDC state, auth code, role resolution, user upsert
- packingService.test.ts — packing item/bag/template operations
- placeService.test.ts — place CRUD and tag attachment
- settingsService.test.ts — settings get/set/bulk
- tagService.test.ts — tag CRUD
- todoService.test.ts — todo CRUD and reorder
- tripService.test.ts — trip CRUD, member management, archiving
- vacayService.test.ts — vacay integration helpers
- tripAccess.test.ts (middleware) — requireTripAccess middleware
Expand existing integration and unit test files with additional cases
across admin, atlas, auth, backup, collab, days, files, maps, memories
(Immich/Synology), notifications, places, reservations, share, vacay,
weather, auth middleware, ephemeral tokens, notification preferences,
permissions, SSRF guard, and WebSocket connection tests.
Update test helpers (factories.ts, test-db.ts) with new factory
functions and seed data required by the expanded suite.
Fix minor issues in server/src/routes/reservations.ts and
server/src/services/atlasService.ts surfaced by new test coverage.
Update sonar-project.properties to reflect new coverage thresholds.
2026-04-07 02:06:46 +08:00
let _categorySeq = 0 ;
let _tagSeq = 0 ;
2026-04-03 19:17:53 +08:00
// ---------------------------------------------------------------------------
// Users
// ---------------------------------------------------------------------------
export interface TestUser {
id : number ;
username : string ;
email : string ;
role : 'admin' | 'user' ;
password_hash : string ;
}
export function createUser (
db : Database.Database ,
overrides : Partial < { username : string ; email : string ; password : string ; role : 'admin' | 'user' } > = { }
) : { user : TestUser ; password : string } {
_userSeq ++ ;
const password = overrides . password ? ? ` TestPass ${ _userSeq } ! ` ;
const email = overrides . email ? ? ` user ${ _userSeq } @test.example.com ` ;
const username = overrides . username ? ? ` testuser ${ _userSeq } ` ;
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 user = db . prepare ( 'SELECT * FROM users WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestUser ;
return { user , password } ;
}
export function createAdmin (
db : Database.Database ,
overrides : Partial < { username : string ; email : string ; password : string } > = { }
) : { user : TestUser ; password : string } {
return createUser ( db , { . . . overrides , role : 'admin' } ) ;
}
/ * *
* Creates a user with MFA already enabled ( directly in DB , bypasses rate - limited HTTP endpoints ) .
* Returns the user , password , and the TOTP secret so tests can generate valid codes .
* /
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' } > = { }
) : { 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 ) ;
const updated = db . prepare ( 'SELECT * FROM users WHERE id = ?' ) . get ( user . id ) as TestUser ;
return { user : updated , password , totpSecret : KNOWN_MFA_SECRET } ;
}
// ---------------------------------------------------------------------------
// Trips
// ---------------------------------------------------------------------------
export interface TestTrip {
id : number ;
user_id : number ;
title : string ;
start_date : string | null ;
end_date : string | null ;
}
export function createTrip (
db : Database.Database ,
userId : number ,
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 ) ;
// Auto-generate days if dates are provided
if ( overrides . start_date && overrides . end_date ) {
const start = new Date ( overrides . start_date ) ;
const end = new Date ( overrides . end_date ) ;
const tripId = result . lastInsertRowid as number ;
let dayNumber = 1 ;
for ( let d = new Date ( start ) ; d <= end ; d . setDate ( d . getDate ( ) + 1 ) ) {
const dateStr = d . toISOString ( ) . slice ( 0 , 10 ) ;
db . prepare ( 'INSERT INTO days (trip_id, day_number, date) VALUES (?, ?, ?)' ) . run ( tripId , dayNumber ++ , dateStr ) ;
}
}
return db . prepare ( 'SELECT * FROM trips WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestTrip ;
}
// ---------------------------------------------------------------------------
// Days
// ---------------------------------------------------------------------------
export interface TestDay {
id : number ;
trip_id : number ;
day_number : number ;
date : string | null ;
title : string | null ;
}
export function createDay (
db : Database.Database ,
tripId : 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 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 ) ;
return db . prepare ( 'SELECT * FROM days WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestDay ;
}
// ---------------------------------------------------------------------------
// Places
// ---------------------------------------------------------------------------
export interface TestPlace {
id : number ;
trip_id : number ;
name : string ;
lat : number | null ;
lng : number | null ;
category_id : number | null ;
}
export function createPlace (
db : Database.Database ,
tripId : number ,
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
) ;
return db . prepare ( 'SELECT * FROM places WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestPlace ;
}
// ---------------------------------------------------------------------------
// Trip Members
// ---------------------------------------------------------------------------
export function addTripMember ( db : Database.Database , tripId : number , userId : number ) : void {
db . prepare ( 'INSERT OR IGNORE INTO trip_members (trip_id, user_id) VALUES (?, ?)' ) . run ( tripId , userId ) ;
}
// ---------------------------------------------------------------------------
// Budget Items
// ---------------------------------------------------------------------------
export interface TestBudgetItem {
id : number ;
trip_id : number ;
name : string ;
category : string ;
total_price : number ;
}
export function createBudgetItem (
db : Database.Database ,
tripId : 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
) ;
return db . prepare ( 'SELECT * FROM budget_items WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestBudgetItem ;
}
// ---------------------------------------------------------------------------
// Packing Items
// ---------------------------------------------------------------------------
export interface TestPackingItem {
id : number ;
trip_id : number ;
name : string ;
category : string ;
checked : number ;
}
export function createPackingItem (
db : Database.Database ,
tripId : number ,
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' ) ;
return db . prepare ( 'SELECT * FROM packing_items WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestPackingItem ;
}
// ---------------------------------------------------------------------------
// Reservations
// ---------------------------------------------------------------------------
export interface TestReservation {
id : number ;
trip_id : number ;
title : string ;
type : string ;
}
export function createReservation (
db : Database.Database ,
tripId : 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 ) ;
return db . prepare ( 'SELECT * FROM reservations WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestReservation ;
}
// ---------------------------------------------------------------------------
// Invite Tokens
// ---------------------------------------------------------------------------
export interface TestInviteToken {
id : number ;
token : string ;
max_uses : number | null ;
used_count : number ;
expires_at : string | null ;
}
2026-04-05 00:12:14 +08:00
// ---------------------------------------------------------------------------
// Day Notes
// ---------------------------------------------------------------------------
export interface TestDayNote {
id : number ;
day_id : number ;
trip_id : number ;
text : string ;
time : string | null ;
icon : string ;
}
export function createDayNote (
db : Database.Database ,
dayId : number ,
tripId : number ,
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 ? ? '📝' ) ;
return db . prepare ( 'SELECT * FROM day_notes WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestDayNote ;
}
// ---------------------------------------------------------------------------
// Collab Notes
// ---------------------------------------------------------------------------
export interface TestCollabNote {
id : number ;
trip_id : number ;
user_id : number ;
title : string ;
content : string | null ;
category : string ;
color : string ;
pinned : number ;
}
export function createCollabNote (
db : Database.Database ,
tripId : number ,
userId : number ,
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'
) ;
return db . prepare ( 'SELECT * FROM collab_notes WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestCollabNote ;
}
2026-04-10 00:09:08 +08:00
// ---------------------------------------------------------------------------
// Todo Items
// ---------------------------------------------------------------------------
export interface TestTodoItem {
id : number ;
trip_id : number ;
name : string ;
checked : number ;
category : string | null ;
sort_order : number ;
}
export function createTodoItem (
db : Database.Database ,
tripId : 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 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 ) ;
return db . prepare ( 'SELECT * FROM todo_items WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestTodoItem ;
}
2026-04-05 00:12:14 +08:00
// ---------------------------------------------------------------------------
// Day Assignments
// ---------------------------------------------------------------------------
export interface TestDayAssignment {
id : number ;
day_id : number ;
place_id : number ;
order_index : number ;
notes : string | null ;
}
export function createDayAssignment (
db : Database.Database ,
dayId : number ,
placeId : number ,
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 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 ) ;
return db . prepare ( 'SELECT * FROM day_assignments WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestDayAssignment ;
}
// ---------------------------------------------------------------------------
// Bucket List
// ---------------------------------------------------------------------------
export interface TestBucketListItem {
id : number ;
user_id : number ;
name : string ;
lat : number | null ;
lng : number | null ;
country_code : string | null ;
notes : string | null ;
}
export function createBucketListItem (
db : Database.Database ,
userId : number ,
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
) ;
return db . prepare ( 'SELECT * FROM bucket_list WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestBucketListItem ;
}
// ---------------------------------------------------------------------------
// 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 ( ) ) ;
}
// ---------------------------------------------------------------------------
// Day Accommodations
// ---------------------------------------------------------------------------
export interface TestDayAccommodation {
id : number ;
trip_id : number ;
place_id : number ;
start_day_id : number ;
end_day_id : number ;
check_in : string | null ;
check_out : string | null ;
}
export function createDayAccommodation (
db : Database.Database ,
tripId : number ,
placeId : number ,
startDayId : number ,
endDayId : number ,
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 ;
}
// ---------------------------------------------------------------------------
// MCP Tokens
// ---------------------------------------------------------------------------
import { createHash } from 'crypto' ;
export interface TestMcpToken {
id : number ;
tokenHash : string ;
rawToken : string ;
}
export function createMcpToken (
db : Database.Database ,
userId : number ,
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' ) ;
return { id : result.lastInsertRowid as number , tokenHash , rawToken } ;
}
// ---------------------------------------------------------------------------
// Invite Tokens
// ---------------------------------------------------------------------------
2026-04-03 19:17:53 +08:00
export function createInviteToken (
db : Database.Database ,
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
let createdBy = overrides . created_by ;
if ( ! createdBy ) {
const admin = db . prepare ( "SELECT id FROM users WHERE role = 'admin' LIMIT 1" ) . get ( ) as { id : number } | undefined ;
if ( admin ) {
createdBy = admin . id ;
} else {
const any = db . prepare ( 'SELECT id FROM users LIMIT 1' ) . get ( ) as { id : number } | undefined ;
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 ( ) ;
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 ) ;
return db . prepare ( 'SELECT * FROM invite_tokens WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestInviteToken ;
}
feat(notifications): add unified multi-channel notification system
Introduces a fully featured notification system with three delivery
channels (in-app, email, webhook), normalized per-user/per-event/
per-channel preferences, admin-scoped notifications, scheduled trip
reminders and version update alerts.
- New notificationService.send() as the single orchestration entry point
- In-app notifications with simple/boolean/navigate types and WebSocket push
- Per-user preference matrix with normalized notification_channel_preferences table
- Admin notification preferences stored globally in app_settings
- Migration 69 normalizes legacy notification_preferences table
- Scheduler hooks for daily trip reminders and version checks
- DevNotificationsPanel for testing in dev mode
- All new tests passing, covering dispatch, preferences, migration, boolean
responses, resilience, and full API integration (NSVC, NPREF, INOTIF,
MIGR, VNOTIF, NROUTE series)
- Previous tests passing
2026-04-05 07:20:33 +08:00
// ---------------------------------------------------------------------------
// Notification helpers
// ---------------------------------------------------------------------------
/** Upsert a key/value pair into app_settings. */
export function setAppSetting ( db : Database.Database , key : string , value : string ) : void {
db . prepare ( 'INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)' ) . run ( key , value ) ;
}
/** Set the active notification channels (e.g. 'email', 'webhook', 'email,webhook', 'none'). */
export function setNotificationChannels ( db : Database.Database , channels : string ) : void {
setAppSetting ( db , 'notification_channels' , channels ) ;
}
/** Explicitly disable a per-user notification preference for a given event+channel combo. */
export function disableNotificationPref (
db : Database.Database ,
userId : number ,
eventType : string ,
channel : string
) : void {
db . prepare (
'INSERT OR REPLACE INTO notification_channel_preferences (user_id, event_type, channel, enabled) VALUES (?, ?, ?, 0)'
) . run ( userId , eventType , channel ) ;
}
2026-04-06 03:11:43 +08:00
// ---------------------------------------------------------------------------
// Photo integration helpers
// ---------------------------------------------------------------------------
export interface TestTripPhoto {
id : number ;
trip_id : number ;
user_id : number ;
asset_id : string ;
provider : string ;
shared : number ;
album_link_id : number | null ;
}
export function addTripPhoto (
db : Database.Database ,
tripId : number ,
userId : number ,
assetId : string ,
provider : string ,
opts : { shared? : boolean ; albumLinkId? : number } = { }
) : TestTripPhoto {
2026-04-14 19:54:48 +08:00
// 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 } ;
2026-04-06 03:11:43 +08:00
const result = db . prepare (
2026-04-14 19:54:48 +08:00
'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;
2026-04-06 03:11:43 +08:00
}
export interface TestAlbumLink {
id : number ;
trip_id : number ;
user_id : number ;
provider : string ;
album_id : string ;
album_name : string ;
}
export function addAlbumLink (
db : Database.Database ,
tripId : number ,
userId : number ,
provider : string ,
albumId : string ,
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 ) ;
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 setSynologyCredentials (
db : Database.Database ,
userId : number ,
url : string ,
username : 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 ) ;
}
test: expand test suite to 87.3% backend coverage
Add new integration test files covering previously untested routes:
- categories.test.ts — GET /api/categories
- oidc.test.ts — full OIDC login flow (callback, state, errors)
- settings.test.ts — GET/PUT /api/settings, bulk save
- tags.test.ts — CRUD for trip tags
- todo.test.ts — todo items CRUD and reorder
Add new unit test files covering service-layer logic:
- adminService.test.ts — user/invite management, packing templates, OIDC settings
- atlasService.test.ts — atlas search and place enrichment
- authServiceDb.test.ts — DB-backed auth helpers (login, register, MFA)
- backupService.test.ts — export/import/restore logic
- categoryService.test.ts — category CRUD
- dayService.test.ts — day management and accommodation helpers
- mapsService.test.ts — route/directions helpers
- oidcService.test.ts — OIDC state, auth code, role resolution, user upsert
- packingService.test.ts — packing item/bag/template operations
- placeService.test.ts — place CRUD and tag attachment
- settingsService.test.ts — settings get/set/bulk
- tagService.test.ts — tag CRUD
- todoService.test.ts — todo CRUD and reorder
- tripService.test.ts — trip CRUD, member management, archiving
- vacayService.test.ts — vacay integration helpers
- tripAccess.test.ts (middleware) — requireTripAccess middleware
Expand existing integration and unit test files with additional cases
across admin, atlas, auth, backup, collab, days, files, maps, memories
(Immich/Synology), notifications, places, reservations, share, vacay,
weather, auth middleware, ephemeral tokens, notification preferences,
permissions, SSRF guard, and WebSocket connection tests.
Update test helpers (factories.ts, test-db.ts) with new factory
functions and seed data required by the expanded suite.
Fix minor issues in server/src/routes/reservations.ts and
server/src/services/atlasService.ts surfaced by new test coverage.
Update sonar-project.properties to reflect new coverage thresholds.
2026-04-07 02:06:46 +08:00
// ---------------------------------------------------------------------------
// Categories
// ---------------------------------------------------------------------------
export function createCategory (
db : Database.Database ,
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 } ;
}
// ---------------------------------------------------------------------------
// Tags
// ---------------------------------------------------------------------------
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 } ;
}
test: comprehensive Journey test suite — 89.5% new code coverage
Server (172 tests):
- journeyService unit tests (87 tests): CRUD, access control, sync, photos, contributors
- journeyShareService unit tests (20 tests): share links, token validation, public access
- journey integration tests (45 tests): all API routes, auth, permissions, edge cases
- Test helpers: journey factories, RESET_TABLES updated
Client (340+ tests):
- journeyStore tests (15 tests): all store actions and state management
- JourneyPage tests (20 tests): frontpage, create flow, suggestions, navigation
- JourneyDetailPage tests (94 tests): all sub-components, entry editor, settings,
share links, contributors, gallery, map, trip linking
- JourneyPublicPage tests (18 tests): public view, tabs, restricted access
- JourneyBookPDF tests (6 tests): PDF generation
- BottomNav tests (9 tests): profile sheet, navigation
- PhotoLightbox tests (8 tests): keyboard nav, counter
- JourneyMap tests (12 tests): markers, polylines, zoom
- Component tests: moodConfig, stripMarkdown, MarkdownToolbar, JournalBody, MobileTopHeader
- DashboardPage tests (32 tests): spotlight card, quick actions, widget settings
SonarQube: exclude unused MemoriesPanel from coverage (dead code, moved to Journey)
2026-04-12 07:19:53 +08:00
// ---------------------------------------------------------------------------
// Journeys
// ---------------------------------------------------------------------------
let _journeySeq = 0 ;
export interface TestJourney {
id : number ;
user_id : number ;
title : string ;
subtitle : string | null ;
status : string ;
cover_image : string | null ;
created_at : number ;
updated_at : number ;
}
export function createJourney (
db : Database.Database ,
userId : number ,
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 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 ) ;
return db . prepare ( 'SELECT * FROM journeys WHERE id = ?' ) . get ( journeyId ) as TestJourney ;
}
export interface TestJourneyEntry {
id : number ;
journey_id : number ;
author_id : number ;
type : string ;
entry_date : string ;
title : string | null ;
story : string | null ;
}
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 } > = { }
) : TestJourneyEntry {
const now = Date . now ( ) ;
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
) ;
return db . prepare ( 'SELECT * FROM journey_entries WHERE id = ?' ) . get ( result . lastInsertRowid ) as TestJourneyEntry ;
}
export function addJourneyContributor (
db : Database.Database ,
journeyId : number ,
userId : number ,
role : 'editor' | 'viewer' = 'editor'
) : void {
db . prepare (
'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 ( ) ) ;
}