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
/ * *
* authServiceDb . test . ts
*
* DB - centric unit tests for authService . ts using a real in - memory SQLite database .
* Pure function tests live in authService . test . ts ( stub DB ) ; this file covers
* functions that require actual DB queries to exercise their logic .
* /
// ---------------------------------------------------------------------------
// vi.hoisted: build the real in-memory DB and the module mock before any import
// ---------------------------------------------------------------------------
const { testDb , dbMock } = vi . hoisted ( ( ) = > {
const Database = require ( 'better-sqlite3' ) ;
const db = new Database ( ':memory:' ) ;
db . exec ( 'PRAGMA journal_mode = WAL' ) ;
db . exec ( 'PRAGMA foreign_keys = ON' ) ;
db . exec ( 'PRAGMA busy_timeout = 5000' ) ;
const mock = {
db ,
closeDb : ( ) = > { } ,
reinitialize : ( ) = > { } ,
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 ) ,
isOwner : ( tripId : any , userId : number ) = >
! ! db . prepare ( 'SELECT id FROM trips WHERE id = ? AND user_id = ?' ) . get ( tripId , userId ) ,
} ;
return { testDb : db , dbMock : mock } ;
} ) ;
vi . mock ( '../../../src/db/database' , ( ) = > dbMock ) ;
vi . mock ( '../../../src/config' , ( ) = > ( {
JWT_SECRET : 'test-secret' ,
ENCRYPTION_KEY : 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2' ,
updateJwtSecret : ( ) = > { } ,
} ) ) ;
vi . mock ( '../../../src/services/mfaCrypto' , ( ) = > ( {
encryptMfaSecret : vi.fn ( ( s ) = > ` enc: ${ s } ` ) ,
decryptMfaSecret : vi.fn ( ( s : string ) = > s . replace ( 'enc:' , '' ) ) ,
} ) ) ;
vi . mock ( '../../../src/services/apiKeyCrypto' , ( ) = > ( {
decrypt_api_key : vi.fn ( ( v ) = > v ) ,
maybe_encrypt_api_key : vi.fn ( ( v ) = > v ) ,
mask_stored_api_key : vi.fn ( ( v : string | null | undefined ) = > ( v ? '••••••••' : null ) ) ,
encrypt_api_key : vi.fn ( ( v ) = > v ) ,
} ) ) ;
vi . mock ( '../../../src/services/permissions' , ( ) = > ( {
getAllPermissions : vi.fn ( ( ) = > ( { } ) ) ,
checkPermission : vi.fn ( ) ,
} ) ) ;
vi . mock ( '../../../src/services/ephemeralTokens' , ( ) = > ( { createEphemeralToken : vi.fn ( ) } ) ) ;
vi . mock ( '../../../src/mcp' , ( ) = > ( { revokeUserSessions : vi.fn ( ) } ) ) ;
vi . mock ( '../../../src/scheduler' , ( ) = > ( {
startTripReminders : vi.fn ( ) ,
buildCronExpression : vi.fn ( ) ,
loadSettings : vi.fn ( ( ) = > ( { enabled : false } ) ) ,
VALID_INTERVALS : [ 'daily' , 'weekly' , 'monthly' ] ,
} ) ) ;
// ---------------------------------------------------------------------------
// Imports (after mocks)
// ---------------------------------------------------------------------------
import { describe , it , expect , beforeAll , beforeEach , afterAll , vi } from 'vitest' ;
import { createTables } from '../../../src/db/schema' ;
import { runMigrations } from '../../../src/db/migrations' ;
import { resetTestDb } from '../../helpers/test-db' ;
import { createUser , createAdmin , createInviteToken } from '../../helpers/factories' ;
import {
updateSettings ,
getSettings ,
listUsers ,
getAppSettings ,
validateKeys ,
isOidcOnlyMode ,
2026-04-12 02:21:22 +08:00
resolveAuthToggles ,
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
setupMfa ,
enableMfa ,
disableMfa ,
validateInviteToken ,
registerUser ,
loginUser ,
changePassword ,
verifyMfaLogin ,
createMcpToken ,
deleteMcpToken ,
} from '../../../src/services/authService' ;
// ---------------------------------------------------------------------------
// Lifecycle
// ---------------------------------------------------------------------------
beforeAll ( ( ) = > {
createTables ( testDb ) ;
runMigrations ( testDb ) ;
} ) ;
beforeEach ( ( ) = > resetTestDb ( testDb ) ) ;
afterAll ( ( ) = > testDb . close ( ) ) ;
// ---------------------------------------------------------------------------
// updateSettings
// ---------------------------------------------------------------------------
describe ( 'updateSettings' , ( ) = > {
it ( 'AUTH-DB-001: updates username successfully' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = updateSettings ( user . id , { username : 'newname' } ) ;
expect ( result . success ) . toBe ( true ) ;
expect ( result . user ? . username ) . toBe ( 'newname' ) ;
} ) ;
it ( 'AUTH-DB-002: returns 400 when username is too short (< 2 chars)' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = updateSettings ( user . id , { username : 'x' } ) ;
expect ( result . status ) . toBe ( 400 ) ;
expect ( result . error ) . toMatch ( /between 2 and 50/i ) ;
} ) ;
it ( 'AUTH-DB-003: returns 400 when username has invalid characters (spaces)' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = updateSettings ( user . id , { username : 'bad name' } ) ;
expect ( result . status ) . toBe ( 400 ) ;
expect ( result . error ) . toMatch ( /only contain/i ) ;
} ) ;
it ( 'AUTH-DB-004: returns 409 when username is already taken by another user' , ( ) = > {
const { user : user1 } = createUser ( testDb , { username : 'alice' } ) ;
const { user : user2 } = createUser ( testDb , { username : 'bob' } ) ;
const result = updateSettings ( user2 . id , { username : user1.username } ) ;
expect ( result . status ) . toBe ( 409 ) ;
expect ( result . error ) . toMatch ( /already taken/i ) ;
} ) ;
it ( 'AUTH-DB-005: updates email successfully' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = updateSettings ( user . id , { email : 'new@example.com' } ) ;
expect ( result . success ) . toBe ( true ) ;
expect ( result . user ? . email ) . toBe ( 'new@example.com' ) ;
} ) ;
it ( 'AUTH-DB-006: returns 400 for invalid email format' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = updateSettings ( user . id , { email : 'not-an-email' } ) ;
expect ( result . status ) . toBe ( 400 ) ;
expect ( result . error ) . toMatch ( /invalid email/i ) ;
} ) ;
it ( 'AUTH-DB-007: returns 409 when email is already taken by another user' , ( ) = > {
const { user : user1 } = createUser ( testDb , { email : 'taken@example.com' } ) ;
const { user : user2 } = createUser ( testDb ) ;
const result = updateSettings ( user2 . id , { email : user1.email } ) ;
expect ( result . status ) . toBe ( 409 ) ;
expect ( result . error ) . toMatch ( /already taken/i ) ;
} ) ;
it ( 'AUTH-DB-008: returns success with no field changes when empty body is passed' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = updateSettings ( user . id , { } ) ;
expect ( result . success ) . toBe ( true ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// getSettings
// ---------------------------------------------------------------------------
describe ( 'getSettings' , ( ) = > {
it ( 'AUTH-DB-009: returns 403 for non-admin user' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = getSettings ( user . id ) ;
expect ( result . status ) . toBe ( 403 ) ;
expect ( result . error ) . toMatch ( /admin/i ) ;
} ) ;
it ( 'AUTH-DB-010: returns maps_api_key and openweather_api_key for admin' , ( ) = > {
const { user } = createAdmin ( testDb ) ;
testDb
. prepare ( 'UPDATE users SET maps_api_key = ?, openweather_api_key = ? WHERE id = ?' )
. run ( 'maps-key-value' , 'weather-key-value' , user . id ) ;
const result = getSettings ( user . id ) ;
expect ( result . status ) . toBeUndefined ( ) ;
expect ( result . settings ) . toBeDefined ( ) ;
expect ( result . settings ) . toHaveProperty ( 'maps_api_key' ) ;
expect ( result . settings ) . toHaveProperty ( 'openweather_api_key' ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// listUsers
// ---------------------------------------------------------------------------
describe ( 'listUsers' , ( ) = > {
it ( 'AUTH-DB-011: returns all users except self, sorted by username' , ( ) = > {
const { user : self } = createUser ( testDb , { username : 'zzself' } ) ;
createUser ( testDb , { username : 'alice' } ) ;
createUser ( testDb , { username : 'charlie' } ) ;
createUser ( testDb , { username : 'bob' } ) ;
const result = listUsers ( self . id ) ;
expect ( result ) . toHaveLength ( 3 ) ;
const names = result . map ( ( u ) = > u . username ) ;
expect ( names ) . toEqual ( [ . . . names ] . sort ( ) ) ;
expect ( names ) . not . toContain ( 'zzself' ) ;
} ) ;
it ( 'AUTH-DB-012: returns empty array when only one user exists' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = listUsers ( user . id ) ;
expect ( result ) . toHaveLength ( 0 ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// getAppSettings
// ---------------------------------------------------------------------------
describe ( 'getAppSettings' , ( ) = > {
it ( 'AUTH-DB-013: returns 403 for non-admin' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = getAppSettings ( user . id ) ;
expect ( result . status ) . toBe ( 403 ) ;
expect ( result . error ) . toMatch ( /admin/i ) ;
} ) ;
it ( 'AUTH-DB-014: returns settings object for admin with known key allow_registration' , ( ) = > {
const { user } = createAdmin ( testDb ) ;
testDb
. prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('allow_registration', 'true')" )
. run ( ) ;
const result = getAppSettings ( user . id ) ;
expect ( result . status ) . toBeUndefined ( ) ;
expect ( result . data ) . toBeDefined ( ) ;
expect ( result . data ) . toHaveProperty ( 'allow_registration' , 'true' ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// validateKeys
// ---------------------------------------------------------------------------
describe ( 'validateKeys' , ( ) = > {
it ( 'AUTH-DB-015: returns 403 for non-admin' , async ( ) = > {
const { user } = createUser ( testDb ) ;
const result = await validateKeys ( user . id ) ;
expect ( result . status ) . toBe ( 403 ) ;
expect ( result . error ) . toMatch ( /admin/i ) ;
expect ( result . maps ) . toBe ( false ) ;
expect ( result . weather ) . toBe ( false ) ;
} ) ;
it ( 'AUTH-DB-016: returns { maps: false, weather: false } when no API keys are stored' , async ( ) = > {
const { user } = createAdmin ( testDb ) ;
const result = await validateKeys ( user . id ) ;
expect ( result . maps ) . toBe ( false ) ;
expect ( result . weather ) . toBe ( false ) ;
expect ( result . maps_details ) . toBeNull ( ) ;
} ) ;
it ( 'AUTH-DB-017: returns { maps: true } when fetch returns 200' , async ( ) = > {
const { user } = createAdmin ( testDb ) ;
testDb . prepare ( 'UPDATE users SET maps_api_key = ? WHERE id = ?' ) . run ( 'test-key' , user . id ) ;
const fetchSpy = vi . spyOn ( global , 'fetch' ) . mockResolvedValueOnce ( {
status : 200 ,
statusText : 'OK' ,
text : async ( ) = > '' ,
} as Response ) ;
const result = await validateKeys ( user . id ) ;
expect ( result . maps ) . toBe ( true ) ;
expect ( result . maps_details ? . ok ) . toBe ( true ) ;
fetchSpy . mockRestore ( ) ;
} ) ;
it ( 'AUTH-DB-018: returns { maps: false } when fetch throws a network error' , async ( ) = > {
const { user } = createAdmin ( testDb ) ;
testDb . prepare ( 'UPDATE users SET maps_api_key = ? WHERE id = ?' ) . run ( 'test-key' , user . id ) ;
const fetchSpy = vi
. spyOn ( global , 'fetch' )
. mockRejectedValueOnce ( new Error ( 'Network failure' ) ) ;
const result = await validateKeys ( user . id ) ;
expect ( result . maps ) . toBe ( false ) ;
expect ( result . maps_details ? . error_status ) . toBe ( 'FETCH_ERROR' ) ;
expect ( result . maps_details ? . error_message ) . toBe ( 'Network failure' ) ;
fetchSpy . mockRestore ( ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// isOidcOnlyMode
// ---------------------------------------------------------------------------
describe ( 'isOidcOnlyMode' , ( ) = > {
it ( 'AUTH-DB-019: returns false when OIDC_ONLY env var is not set' , ( ) = > {
vi . stubEnv ( 'OIDC_ONLY' , '' ) ;
expect ( isOidcOnlyMode ( ) ) . toBe ( false ) ;
vi . unstubAllEnvs ( ) ;
} ) ;
it ( 'AUTH-DB-020: returns false when OIDC_ONLY=true but no OIDC_ISSUER configured' , ( ) = > {
vi . stubEnv ( 'OIDC_ONLY' , 'true' ) ;
vi . stubEnv ( 'OIDC_ISSUER' , '' ) ;
vi . stubEnv ( 'OIDC_CLIENT_ID' , '' ) ;
expect ( isOidcOnlyMode ( ) ) . toBe ( false ) ;
vi . unstubAllEnvs ( ) ;
} ) ;
it ( 'AUTH-DB-021: returns true when OIDC_ONLY=true AND OIDC_ISSUER AND OIDC_CLIENT_ID are set' , ( ) = > {
vi . stubEnv ( 'OIDC_ONLY' , 'true' ) ;
vi . stubEnv ( 'OIDC_ISSUER' , 'https://sso.example.com' ) ;
vi . stubEnv ( 'OIDC_CLIENT_ID' , 'trek-client' ) ;
expect ( isOidcOnlyMode ( ) ) . toBe ( true ) ;
vi . unstubAllEnvs ( ) ;
} ) ;
} ) ;
2026-04-12 02:21:22 +08:00
// ---------------------------------------------------------------------------
// resolveAuthToggles
// ---------------------------------------------------------------------------
describe ( 'resolveAuthToggles' , ( ) = > {
afterEach ( ( ) = > {
vi . unstubAllEnvs ( ) ;
testDb . prepare ( "DELETE FROM app_settings WHERE key IN ('password_login','password_registration','oidc_login','oidc_registration','oidc_only','allow_registration')" ) . run ( ) ;
} ) ;
it ( 'AUTH-DB-022a: returns all true by default (no DB keys, no env override)' , ( ) = > {
vi . stubEnv ( 'OIDC_ONLY' , '' ) ;
const t = resolveAuthToggles ( ) ;
expect ( t . password_login ) . toBe ( true ) ;
expect ( t . password_registration ) . toBe ( true ) ;
expect ( t . oidc_login ) . toBe ( true ) ;
expect ( t . oidc_registration ) . toBe ( true ) ;
} ) ;
it ( 'AUTH-DB-022b: legacy — OIDC_ONLY=true with OIDC configured disables password_login and password_registration' , ( ) = > {
vi . stubEnv ( 'OIDC_ONLY' , 'true' ) ;
vi . stubEnv ( 'OIDC_ISSUER' , 'https://sso.example.com' ) ;
vi . stubEnv ( 'OIDC_CLIENT_ID' , 'trek-client' ) ;
const t = resolveAuthToggles ( ) ;
expect ( t . password_login ) . toBe ( false ) ;
expect ( t . password_registration ) . toBe ( false ) ;
expect ( t . oidc_login ) . toBe ( true ) ;
expect ( t . oidc_registration ) . toBe ( true ) ;
} ) ;
it ( 'AUTH-DB-022c: legacy — allow_registration=false disables both password and oidc registration' , ( ) = > {
vi . stubEnv ( 'OIDC_ONLY' , '' ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('allow_registration', 'false')" ) . run ( ) ;
const t = resolveAuthToggles ( ) ;
expect ( t . password_login ) . toBe ( true ) ;
expect ( t . password_registration ) . toBe ( false ) ;
expect ( t . oidc_login ) . toBe ( true ) ;
expect ( t . oidc_registration ) . toBe ( false ) ;
} ) ;
it ( 'AUTH-DB-022d: new granular keys take precedence over legacy keys' , ( ) = > {
vi . stubEnv ( 'OIDC_ONLY' , '' ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('allow_registration', 'false')" ) . run ( ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('password_registration', 'true')" ) . run ( ) ;
const t = resolveAuthToggles ( ) ;
// New key present → use new keys, allow_registration ignored
expect ( t . password_registration ) . toBe ( true ) ;
expect ( t . oidc_registration ) . toBe ( true ) ; // defaults to true when key not set
} ) ;
it ( 'AUTH-DB-022e: OIDC_ONLY env var overrides new granular keys for password toggles' , ( ) = > {
vi . stubEnv ( 'OIDC_ONLY' , 'true' ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('password_login', 'true')" ) . run ( ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('password_registration', 'true')" ) . run ( ) ;
const t = resolveAuthToggles ( ) ;
// OIDC_ONLY forces password toggles off even when DB says true
expect ( t . password_login ) . toBe ( false ) ;
expect ( t . password_registration ) . toBe ( false ) ;
} ) ;
it ( 'AUTH-DB-022f: individual granular keys can be set independently' , ( ) = > {
vi . stubEnv ( 'OIDC_ONLY' , '' ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('password_login', 'true')" ) . run ( ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('password_registration', 'false')" ) . run ( ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('oidc_login', 'true')" ) . run ( ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('oidc_registration', 'false')" ) . run ( ) ;
const t = resolveAuthToggles ( ) ;
expect ( t . password_login ) . toBe ( true ) ;
expect ( t . password_registration ) . toBe ( false ) ;
expect ( t . oidc_login ) . toBe ( true ) ;
expect ( t . oidc_registration ) . toBe ( false ) ;
} ) ;
} ) ;
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
// ---------------------------------------------------------------------------
// setupMfa
// ---------------------------------------------------------------------------
describe ( 'setupMfa' , ( ) = > {
it ( 'AUTH-DB-022: returns 403 in demo mode for demo@nomad.app' , ( ) = > {
vi . stubEnv ( 'DEMO_MODE' , 'true' ) ;
const { user } = createUser ( testDb , { email : 'demo@nomad.app' } ) ;
const result = setupMfa ( user . id , 'demo@nomad.app' ) ;
expect ( result . status ) . toBe ( 403 ) ;
expect ( result . error ) . toMatch ( /demo mode/i ) ;
vi . unstubAllEnvs ( ) ;
} ) ;
it ( 'AUTH-DB-023: returns 400 when MFA is already enabled' , ( ) = > {
const { user } = createUser ( testDb ) ;
testDb . prepare ( 'UPDATE users SET mfa_enabled = 1 WHERE id = ?' ) . run ( user . id ) ;
const result = setupMfa ( user . id , user . email ) ;
expect ( result . status ) . toBe ( 400 ) ;
expect ( result . error ) . toMatch ( /already enabled/i ) ;
} ) ;
it ( 'AUTH-DB-024: returns secret and otpauth_url when MFA setup starts successfully' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = setupMfa ( user . id , user . email ) ;
expect ( result . error ) . toBeUndefined ( ) ;
expect ( typeof result . secret ) . toBe ( 'string' ) ;
expect ( result . secret ! . length ) . toBeGreaterThan ( 0 ) ;
expect ( typeof result . otpauth_url ) . toBe ( 'string' ) ;
expect ( result . otpauth_url ) . toMatch ( /^otpauth:\/\/totp\// ) ;
expect ( result . qrPromise ) . toBeInstanceOf ( Promise ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// enableMfa
// ---------------------------------------------------------------------------
describe ( 'enableMfa' , ( ) = > {
it ( 'AUTH-DB-025: returns 400 when no verification code is provided' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = enableMfa ( user . id , undefined ) ;
expect ( result . status ) . toBe ( 400 ) ;
expect ( result . error ) . toMatch ( /code is required/i ) ;
} ) ;
it ( 'AUTH-DB-026: returns 400 when there is no pending MFA setup' , ( ) = > {
const { user } = createUser ( testDb ) ;
// No setupMfa called first, so no pending entry exists
const result = enableMfa ( user . id , '123456' ) ;
expect ( result . status ) . toBe ( 400 ) ;
expect ( result . error ) . toMatch ( /no mfa setup in progress/i ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// disableMfa
// ---------------------------------------------------------------------------
describe ( 'disableMfa' , ( ) = > {
it ( 'AUTH-DB-027: returns 403 in demo mode for demo@nomad.app' , ( ) = > {
vi . stubEnv ( 'DEMO_MODE' , 'true' ) ;
const { user } = createUser ( testDb , { email : 'demo@nomad.app' } ) ;
const result = disableMfa ( user . id , 'demo@nomad.app' , {
password : 'password123' ,
code : '000000' ,
} ) ;
expect ( result . status ) . toBe ( 403 ) ;
expect ( result . error ) . toMatch ( /demo mode/i ) ;
vi . unstubAllEnvs ( ) ;
} ) ;
it ( 'AUTH-DB-028: returns 400 when password or code is missing' , ( ) = > {
const { user } = createUser ( testDb ) ;
const missingCode = disableMfa ( user . id , user . email , { password : 'pass' , code : undefined } ) ;
expect ( missingCode . status ) . toBe ( 400 ) ;
expect ( missingCode . error ) . toMatch ( /password and authenticator code/i ) ;
const missingPassword = disableMfa ( user . id , user . email , { password : undefined , code : '123456' } ) ;
expect ( missingPassword . status ) . toBe ( 400 ) ;
expect ( missingPassword . error ) . toMatch ( /password and authenticator code/i ) ;
} ) ;
it ( 'AUTH-DB-029: returns 400 when MFA is not enabled on the account' , ( ) = > {
const { user } = createUser ( testDb ) ;
// mfa_enabled defaults to 0 / not set
const result = disableMfa ( user . id , user . email , { password : 'password123' , code : '000000' } ) ;
expect ( result . status ) . toBe ( 400 ) ;
expect ( result . error ) . toMatch ( /not enabled/i ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// validateInviteToken
// ---------------------------------------------------------------------------
describe ( 'validateInviteToken' , ( ) = > {
it ( 'AUTH-DB-030: returns 404 for unknown token' , ( ) = > {
const result = validateInviteToken ( 'no-such-token' ) ;
expect ( result . status ) . toBe ( 404 ) ;
} ) ;
it ( 'AUTH-DB-031: returns 410 when max_uses exceeded' , ( ) = > {
// createInviteToken with used_count already at max
const invite = createInviteToken ( testDb , { max_uses : 1 } ) ;
// manually set used_count = 1 to simulate exhaustion
testDb . prepare ( 'UPDATE invite_tokens SET used_count = 1 WHERE id = ?' ) . run ( invite . id ) ;
const result = validateInviteToken ( invite . token ) ;
expect ( result . status ) . toBe ( 410 ) ;
} ) ;
it ( 'AUTH-DB-032: returns 410 when expired' , ( ) = > {
const invite = createInviteToken ( testDb , { expires_at : '2000-01-01T00:00:00.000Z' } ) ;
const result = validateInviteToken ( invite . token ) ;
expect ( result . status ) . toBe ( 410 ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// registerUser — OIDC-only / registration-disabled
// ---------------------------------------------------------------------------
describe ( 'registerUser — OIDC-only / registration-disabled' , ( ) = > {
it ( 'AUTH-DB-033: returns 403 when oidc_only=true and not first user' , ( ) = > {
createUser ( testDb ) ; // ensure userCount > 0
testDb . prepare ( "INSERT INTO app_settings (key, value) VALUES ('oidc_only', 'true')" ) . run ( ) ;
testDb . prepare ( "INSERT INTO app_settings (key, value) VALUES ('oidc_issuer', 'https://x')" ) . run ( ) ;
testDb . prepare ( "INSERT INTO app_settings (key, value) VALUES ('oidc_client_id', 'id')" ) . run ( ) ;
const result = registerUser ( { username : 'u' , email : 'new@x.com' , password : 'Secure123!' } ) ;
expect ( result . status ) . toBe ( 403 ) ;
2026-04-12 02:21:22 +08:00
expect ( result . error ) . toMatch ( /password registration is disabled/i ) ;
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
} ) ;
it ( 'AUTH-DB-034: returns 403 when registration is disabled and no invite' , ( ) = > {
createUser ( testDb ) ; // ensure userCount > 0
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('allow_registration', 'false')" ) . run ( ) ;
const result = registerUser ( { username : 'u2' , email : 'n2@x.com' , password : 'Secure123!' } ) ;
expect ( result . status ) . toBe ( 403 ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// loginUser — OIDC-only mode
// ---------------------------------------------------------------------------
describe ( 'loginUser — OIDC-only mode' , ( ) = > {
it ( 'AUTH-DB-035: returns 403 when oidc_only=true' , ( ) = > {
const { user , password } = createUser ( testDb ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('oidc_only', 'true')" ) . run ( ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('oidc_issuer', 'https://x')" ) . run ( ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('oidc_client_id', 'id')" ) . run ( ) ;
const result = loginUser ( { email : user.email , password } ) ;
expect ( result . status ) . toBe ( 403 ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// changePassword — OIDC-only mode
// ---------------------------------------------------------------------------
describe ( 'changePassword — OIDC-only mode' , ( ) = > {
it ( 'AUTH-DB-036: returns 403 when oidc_only=true' , ( ) = > {
const { user , password } = createUser ( testDb ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('oidc_only', 'true')" ) . run ( ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('oidc_issuer', 'https://x')" ) . run ( ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('oidc_client_id', 'id')" ) . run ( ) ;
const result = changePassword ( user . id , user . email , { current_password : password , new_password : 'New1234!' } ) ;
expect ( result . status ) . toBe ( 403 ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// disableMfa — require_mfa policy
// ---------------------------------------------------------------------------
describe ( 'disableMfa — require_mfa policy' , ( ) = > {
it ( 'AUTH-DB-037: returns 403 when require_mfa=true is set globally' , ( ) = > {
const { user } = createUser ( testDb ) ;
testDb . prepare ( "INSERT OR REPLACE INTO app_settings (key, value) VALUES ('require_mfa', 'true')" ) . run ( ) ;
const result = disableMfa ( user . id , user . email , { password : 'pass' , code : '123456' } ) ;
expect ( result . status ) . toBe ( 403 ) ;
expect ( result . error ) . toMatch ( /cannot be disabled/i ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// verifyMfaLogin — validation
// ---------------------------------------------------------------------------
describe ( 'verifyMfaLogin — validation' , ( ) = > {
it ( 'AUTH-DB-038: returns 400 when mfa_token or code is missing' , ( ) = > {
const result = verifyMfaLogin ( { mfa_token : undefined , code : undefined } ) ;
expect ( result . status ) . toBe ( 400 ) ;
} ) ;
it ( 'AUTH-DB-039: returns 401 when mfa_token has wrong purpose' , ( ) = > {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const jwt = require ( 'jsonwebtoken' ) ;
const tok = jwt . sign ( { id : 1 , purpose : 'wrong' } , 'test-secret' , { expiresIn : '5m' , algorithm : 'HS256' } ) ;
const result = verifyMfaLogin ( { mfa_token : tok , code : '123456' } ) ;
expect ( result . status ) . toBe ( 401 ) ;
expect ( result . error ) . toMatch ( /invalid/i ) ;
} ) ;
it ( 'AUTH-DB-040: returns 401 when user not found for valid mfa_token' , ( ) = > {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const jwt = require ( 'jsonwebtoken' ) ;
const tok = jwt . sign ( { id : 99999 , purpose : 'mfa_login' } , 'test-secret' , { expiresIn : '5m' , algorithm : 'HS256' } ) ;
const result = verifyMfaLogin ( { mfa_token : tok , code : '123456' } ) ;
expect ( result . status ) . toBe ( 401 ) ;
} ) ;
} ) ;
// ---------------------------------------------------------------------------
// MCP token service
// ---------------------------------------------------------------------------
describe ( 'MCP token service' , ( ) = > {
it ( 'AUTH-DB-041: createMcpToken returns 400 when name is missing' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = createMcpToken ( user . id , undefined ) ;
expect ( result . status ) . toBe ( 400 ) ;
} ) ;
it ( 'AUTH-DB-042: createMcpToken returns 400 when name exceeds 100 chars' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = createMcpToken ( user . id , 'a' . repeat ( 101 ) ) ;
expect ( result . status ) . toBe ( 400 ) ;
} ) ;
it ( 'AUTH-DB-043: createMcpToken creates token and returns raw_token' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = createMcpToken ( user . id , 'My Token' ) ;
expect ( result . token ) . toBeDefined ( ) ;
expect ( ( result . token as any ) . raw_token ) . toMatch ( /^trek_/ ) ;
} ) ;
it ( 'AUTH-DB-044: createMcpToken returns 400 when user has 10 tokens already' , ( ) = > {
const { user } = createUser ( testDb ) ;
for ( let i = 0 ; i < 10 ; i ++ ) {
testDb . prepare (
'INSERT INTO mcp_tokens (user_id, name, token_hash, token_prefix) VALUES (?, ?, ?, ?)'
) . run ( user . id , ` Token ${ i } ` , ` hash ${ i } ` , ` trek_prefix ${ i } ` ) ;
}
const result = createMcpToken ( user . id , 'One More' ) ;
expect ( result . status ) . toBe ( 400 ) ;
} ) ;
it ( 'AUTH-DB-045: deleteMcpToken returns 404 for non-existent token' , ( ) = > {
const { user } = createUser ( testDb ) ;
const result = deleteMcpToken ( user . id , '99999' ) ;
expect ( result . status ) . toBe ( 404 ) ;
} ) ;
it ( 'AUTH-DB-046: deleteMcpToken deletes the token and returns success' , ( ) = > {
const { user } = createUser ( testDb ) ;
const created = createMcpToken ( user . id , 'Deletable Token' ) ;
const tokenId = String ( ( created . token as any ) . id ) ;
const result = deleteMcpToken ( user . id , tokenId ) ;
expect ( result ) . toEqual ( { success : true } ) ;
const row = testDb . prepare ( 'SELECT id FROM mcp_tokens WHERE id = ?' ) . get ( tokenId ) ;
expect ( row ) . toBeUndefined ( ) ;
} ) ;
} ) ;