Spaces:
Sleeping
Sleeping
| /** | |
| * Unit tests for adminService β ADMIN-SVC-001 through ADMIN-SVC-050. | |
| * Uses a real in-memory SQLite DB. Focuses on validation/error branches | |
| * that the integration tests don't exercise. | |
| */ | |
| import { describe, it, expect, vi, beforeAll, beforeEach, afterEach, afterAll } from 'vitest'; | |
| // ββ DB setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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: () => {}, | |
| getPlaceWithTags: () => null, | |
| canAccessTrip: () => null, | |
| isOwner: () => false, | |
| }; | |
| 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/apiKeyCrypto', () => ({ | |
| encrypt_api_key: (v: string) => v, | |
| decrypt_api_key: (v: string) => v, | |
| maybe_encrypt_api_key: (v: string) => v, | |
| })); | |
| vi.mock('../../../src/mcp', () => ({ | |
| revokeUserSessions: vi.fn(), | |
| })); | |
| vi.mock('../../../src/demo/demo-reset', () => ({ | |
| saveBaseline: vi.fn(), | |
| })); | |
| 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 { | |
| listUsers, | |
| createUser as svcCreateUser, | |
| updateUser, | |
| deleteUser, | |
| getStats, | |
| getPermissions, | |
| savePermissions, | |
| getAuditLog, | |
| listInvites, | |
| createInvite, | |
| deleteInvite, | |
| getBagTracking, | |
| updateBagTracking, | |
| listPackingTemplates, | |
| createPackingTemplate, | |
| updatePackingTemplate, | |
| deletePackingTemplate, | |
| createTemplateCategory, | |
| updateTemplateCategory, | |
| deleteTemplateCategory, | |
| getPackingTemplate, | |
| createTemplateItem, | |
| updateTemplateItem, | |
| deleteTemplateItem, | |
| getOidcSettings, | |
| updateOidcSettings, | |
| saveDemoBaseline, | |
| getGithubReleases, | |
| checkVersion, | |
| listAddons, | |
| updateAddon, | |
| listMcpTokens, | |
| deleteMcpToken, | |
| } from '../../../src/services/adminService'; | |
| beforeAll(() => { | |
| createTables(testDb); | |
| runMigrations(testDb); | |
| }); | |
| beforeEach(() => { | |
| resetTestDb(testDb); | |
| }); | |
| afterAll(() => { | |
| testDb.close(); | |
| }); | |
| // ββ listUsers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('listUsers', () => { | |
| it('ADMIN-SVC-001 β returns all users with online:false', () => { | |
| createUser(testDb); | |
| createUser(testDb); | |
| const users = listUsers() as any[]; | |
| expect(users.length).toBeGreaterThanOrEqual(2); | |
| expect(users.every((u: any) => u.online === false)).toBe(true); | |
| }); | |
| }); | |
| // ββ createUser ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('createUser (service)', () => { | |
| it('ADMIN-SVC-002 β creates a user successfully', () => { | |
| const result = svcCreateUser({ username: 'newuser', email: 'new@test.com', password: 'ValidPass1!' }) as any; | |
| expect(result.user).toBeDefined(); | |
| expect(result.user.email).toBe('new@test.com'); | |
| }); | |
| it('ADMIN-SVC-003 β returns 400 when username is missing', () => { | |
| const result = svcCreateUser({ username: '', email: 'x@x.com', password: 'ValidPass1!' }) as any; | |
| expect(result.status).toBe(400); | |
| }); | |
| it('ADMIN-SVC-004 β returns 400 for invalid role', () => { | |
| const result = svcCreateUser({ username: 'u1', email: 'u1@test.com', password: 'ValidPass1!', role: 'superuser' }) as any; | |
| expect(result.status).toBe(400); | |
| expect(result.error).toMatch(/invalid role/i); | |
| }); | |
| it('ADMIN-SVC-005 β returns 409 for duplicate username', () => { | |
| createUser(testDb); | |
| const { user } = createUser(testDb); | |
| const result = svcCreateUser({ username: user.username, email: 'unique@test.com', password: 'ValidPass1!' }) as any; | |
| expect(result.status).toBe(409); | |
| }); | |
| it('ADMIN-SVC-006 β returns 409 for duplicate email', () => { | |
| const { user } = createUser(testDb); | |
| const result = svcCreateUser({ username: 'uniqueuser', email: user.email, password: 'ValidPass1!' }) as any; | |
| expect(result.status).toBe(409); | |
| }); | |
| it('ADMIN-SVC-007 β returns 400 for weak password', () => { | |
| const result = svcCreateUser({ username: 'weakpwuser', email: 'weakpw@test.com', password: 'short' }) as any; | |
| expect(result.status).toBe(400); | |
| }); | |
| }); | |
| // ββ updateUser ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('updateUser', () => { | |
| it('ADMIN-SVC-008 β updates username successfully', () => { | |
| const { user } = createUser(testDb); | |
| const result = updateUser(String(user.id), { username: 'updatedname' }) as any; | |
| expect(result.user).toBeDefined(); | |
| expect(result.user.username).toBe('updatedname'); | |
| }); | |
| it('ADMIN-SVC-009 β returns 404 for non-existent user', () => { | |
| const result = updateUser('99999', { username: 'ghost' }) as any; | |
| expect(result.status).toBe(404); | |
| }); | |
| it('ADMIN-SVC-010 β returns 400 for invalid role', () => { | |
| const { user } = createUser(testDb); | |
| const result = updateUser(String(user.id), { role: 'superadmin' }) as any; | |
| expect(result.status).toBe(400); | |
| }); | |
| it('ADMIN-SVC-011 β returns 409 when username is taken', () => { | |
| const { user: u1 } = createUser(testDb); | |
| const { user: u2 } = createUser(testDb); | |
| const result = updateUser(String(u2.id), { username: u1.username }) as any; | |
| expect(result.status).toBe(409); | |
| }); | |
| it('ADMIN-SVC-012 β returns 409 when email is taken', () => { | |
| const { user: u1 } = createUser(testDb); | |
| const { user: u2 } = createUser(testDb); | |
| const result = updateUser(String(u2.id), { email: u1.email }) as any; | |
| expect(result.status).toBe(409); | |
| }); | |
| it('ADMIN-SVC-013 β returns 400 for weak password', () => { | |
| const { user } = createUser(testDb); | |
| const result = updateUser(String(user.id), { password: 'weak' }) as any; | |
| expect(result.status).toBe(400); | |
| }); | |
| it('ADMIN-SVC-014 β tracks changed fields in result', () => { | |
| const { user } = createUser(testDb); | |
| const result = updateUser(String(user.id), { username: 'newname', role: 'admin' }) as any; | |
| expect(result.changed).toContain('username'); | |
| expect(result.changed).toContain('role'); | |
| }); | |
| }); | |
| // ββ deleteUser ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('deleteUser', () => { | |
| it('ADMIN-SVC-015 β deletes user successfully', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const { user } = createUser(testDb); | |
| const result = deleteUser(String(user.id), admin.id) as any; | |
| expect(result.email).toBe(user.email); | |
| }); | |
| it('ADMIN-SVC-016 β returns 400 when deleting own account', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const result = deleteUser(String(admin.id), admin.id) as any; | |
| expect(result.status).toBe(400); | |
| }); | |
| it('ADMIN-SVC-017 β returns 404 for non-existent user', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const result = deleteUser('99999', admin.id) as any; | |
| expect(result.status).toBe(404); | |
| }); | |
| }); | |
| // ββ getStats ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('getStats', () => { | |
| it('ADMIN-SVC-018 β returns numeric counts for all stats', () => { | |
| const stats = getStats() as any; | |
| expect(typeof stats.totalUsers).toBe('number'); | |
| expect(typeof stats.totalTrips).toBe('number'); | |
| expect(typeof stats.totalPlaces).toBe('number'); | |
| expect(typeof stats.totalFiles).toBe('number'); | |
| }); | |
| }); | |
| // ββ getPermissions / savePermissions βββββββββββββββββββββββββββββββββββββββββ | |
| describe('Permissions', () => { | |
| it('ADMIN-SVC-019 β getPermissions returns an array of actions', () => { | |
| const result = getPermissions() as any; | |
| expect(Array.isArray(result.permissions)).toBe(true); | |
| expect(result.permissions.length).toBeGreaterThan(0); | |
| }); | |
| it('ADMIN-SVC-020 β savePermissions persists a permission change', () => { | |
| savePermissions({ trip_create: 'admin' }); | |
| const result = getPermissions() as any; | |
| const perm = result.permissions.find((p: any) => p.key === 'trip_create'); | |
| expect(perm.level).toBe('admin'); | |
| }); | |
| }); | |
| // ββ getAuditLog βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('getAuditLog', () => { | |
| it('ADMIN-SVC-021 β returns entries array with total', () => { | |
| const result = getAuditLog({}) as any; | |
| expect(Array.isArray(result.entries)).toBe(true); | |
| expect(typeof result.total).toBe('number'); | |
| expect(result.limit).toBe(100); | |
| expect(result.offset).toBe(0); | |
| }); | |
| it('ADMIN-SVC-022 β respects limit and offset params', () => { | |
| const result = getAuditLog({ limit: '10', offset: '0' }) as any; | |
| expect(result.limit).toBe(10); | |
| expect(result.offset).toBe(0); | |
| }); | |
| it('ADMIN-SVC-023 β caps limit at 500', () => { | |
| const result = getAuditLog({ limit: '9999' }) as any; | |
| expect(result.limit).toBe(500); | |
| }); | |
| }); | |
| // ββ Invites βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('Invites', () => { | |
| it('ADMIN-SVC-024 β createInvite returns invite with token', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const result = createInvite(admin.id, { max_uses: 5 }) as any; | |
| expect(result.invite.token).toBeDefined(); | |
| expect(result.invite.max_uses).toBe(5); | |
| }); | |
| it('ADMIN-SVC-025 β createInvite defaults to 1 use', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const result = createInvite(admin.id, {}) as any; | |
| expect(result.uses).toBe(1); | |
| }); | |
| it('ADMIN-SVC-026 β listInvites returns array', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| createInvite(admin.id, {}); | |
| const invites = listInvites() as any[]; | |
| expect(invites.length).toBeGreaterThanOrEqual(1); | |
| }); | |
| it('ADMIN-SVC-027 β deleteInvite removes invite', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const invite = createInviteToken(testDb, { created_by: admin.id }) as any; | |
| const result = deleteInvite(String(invite.id)) as any; | |
| expect(result.error).toBeUndefined(); | |
| const check = testDb.prepare('SELECT id FROM invite_tokens WHERE id = ?').get(invite.id); | |
| expect(check).toBeUndefined(); | |
| }); | |
| it('ADMIN-SVC-028 β deleteInvite returns 404 for non-existent invite', () => { | |
| const result = deleteInvite('99999') as any; | |
| expect(result.status).toBe(404); | |
| }); | |
| }); | |
| // ββ Bag tracking ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('Bag tracking', () => { | |
| it('ADMIN-SVC-029 β getBagTracking returns enabled state', () => { | |
| const result = getBagTracking() as any; | |
| expect(typeof result.enabled).toBe('boolean'); | |
| }); | |
| it('ADMIN-SVC-030 β updateBagTracking persists the value', () => { | |
| updateBagTracking(true); | |
| expect((getBagTracking() as any).enabled).toBe(true); | |
| updateBagTracking(false); | |
| expect((getBagTracking() as any).enabled).toBe(false); | |
| }); | |
| }); | |
| // ββ Packing templates βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('Packing templates', () => { | |
| it('ADMIN-SVC-031 β createPackingTemplate returns template', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const result = createPackingTemplate('Beach Trip', admin.id) as any; | |
| expect(result.template.name).toBe('Beach Trip'); | |
| }); | |
| it('ADMIN-SVC-032 β createPackingTemplate returns 400 for empty name', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const result = createPackingTemplate('', admin.id) as any; | |
| expect(result.status).toBe(400); | |
| }); | |
| it('ADMIN-SVC-033 β listPackingTemplates returns array', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| createPackingTemplate('Template A', admin.id); | |
| const templates = listPackingTemplates() as any[]; | |
| expect(templates.length).toBeGreaterThanOrEqual(1); | |
| }); | |
| it('ADMIN-SVC-034 β updatePackingTemplate updates name', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const created = createPackingTemplate('Old Name', admin.id) as any; | |
| const result = updatePackingTemplate(String(created.template.id), { name: 'New Name' }) as any; | |
| expect(result.template.name).toBe('New Name'); | |
| }); | |
| it('ADMIN-SVC-035 β updatePackingTemplate returns 404 for non-existent', () => { | |
| const result = updatePackingTemplate('99999', { name: 'Ghost' }) as any; | |
| expect(result.status).toBe(404); | |
| }); | |
| it('ADMIN-SVC-036 β deletePackingTemplate removes template', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const created = createPackingTemplate('To Delete', admin.id) as any; | |
| const result = deletePackingTemplate(String(created.template.id)) as any; | |
| expect(result.name).toBe('To Delete'); | |
| }); | |
| it('ADMIN-SVC-037 β deletePackingTemplate returns 404 for non-existent', () => { | |
| const result = deletePackingTemplate('99999') as any; | |
| expect(result.status).toBe(404); | |
| }); | |
| }); | |
| // ββ Template categories βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('Template categories', () => { | |
| it('ADMIN-SVC-038 β createTemplateCategory creates a category', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const tpl = createPackingTemplate('Tpl', admin.id) as any; | |
| const result = createTemplateCategory(String(tpl.template.id), 'Clothing') as any; | |
| expect(result.category.name).toBe('Clothing'); | |
| }); | |
| it('ADMIN-SVC-039 β createTemplateCategory returns 400 for empty name', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const tpl = createPackingTemplate('Tpl', admin.id) as any; | |
| const result = createTemplateCategory(String(tpl.template.id), '') as any; | |
| expect(result.status).toBe(400); | |
| }); | |
| it('ADMIN-SVC-040 β createTemplateCategory returns 404 for missing template', () => { | |
| const result = createTemplateCategory('99999', 'Clothing') as any; | |
| expect(result.status).toBe(404); | |
| }); | |
| it('ADMIN-SVC-041 β updateTemplateCategory updates name', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const tpl = createPackingTemplate('Tpl', admin.id) as any; | |
| const cat = createTemplateCategory(String(tpl.template.id), 'Old') as any; | |
| const result = updateTemplateCategory(String(tpl.template.id), String(cat.category.id), { name: 'New' }) as any; | |
| expect(result.category.name).toBe('New'); | |
| }); | |
| it('ADMIN-SVC-042 β updateTemplateCategory returns 404 for missing category', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const tpl = createPackingTemplate('Tpl', admin.id) as any; | |
| const result = updateTemplateCategory(String(tpl.template.id), '99999', { name: 'X' }) as any; | |
| expect(result.status).toBe(404); | |
| }); | |
| it('ADMIN-SVC-043 β deleteTemplateCategory removes category', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const tpl = createPackingTemplate('Tpl', admin.id) as any; | |
| const cat = createTemplateCategory(String(tpl.template.id), 'Remove Me') as any; | |
| const result = deleteTemplateCategory(String(tpl.template.id), String(cat.category.id)) as any; | |
| expect(result.error).toBeUndefined(); | |
| }); | |
| it('ADMIN-SVC-044 β deleteTemplateCategory returns 404 for missing', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const tpl = createPackingTemplate('Tpl', admin.id) as any; | |
| const result = deleteTemplateCategory(String(tpl.template.id), '99999') as any; | |
| expect(result.status).toBe(404); | |
| }); | |
| }); | |
| // ββ getAuditLog β JSON details parsing βββββββββββββββββββββββββββββββββββββββ | |
| describe('getAuditLog β JSON details', () => { | |
| it('ADMIN-SVC-045 β parses JSON details when present', () => { | |
| const { user } = createUser(testDb); | |
| testDb.prepare('INSERT INTO audit_log (user_id, action, details) VALUES (?, ?, ?)').run( | |
| user.id, 'test_action', JSON.stringify({ key: 'val' }) | |
| ); | |
| const result = getAuditLog({}) as any; | |
| expect(result.entries.length).toBeGreaterThanOrEqual(1); | |
| const entry = result.entries.find((e: any) => e.action === 'test_action'); | |
| expect(entry).toBeDefined(); | |
| expect(entry.details).toEqual({ key: 'val' }); | |
| }); | |
| it('ADMIN-SVC-046 β handles invalid JSON gracefully with _parse_error flag', () => { | |
| const { user } = createUser(testDb); | |
| testDb.prepare('INSERT INTO audit_log (user_id, action, details) VALUES (?, ?, ?)').run( | |
| user.id, 'bad_json_action', 'not-valid-json{' | |
| ); | |
| const result = getAuditLog({}) as any; | |
| const entry = result.entries.find((e: any) => e.action === 'bad_json_action'); | |
| expect(entry).toBeDefined(); | |
| expect(entry.details).toEqual({ _parse_error: true }); | |
| }); | |
| }); | |
| // ββ OIDC Settings βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('OIDC Settings', () => { | |
| it('ADMIN-SVC-047 β getOidcSettings returns default empty values when no OIDC configured', () => { | |
| const result = getOidcSettings() as any; | |
| expect(result.issuer).toBe(''); | |
| expect(result.client_id).toBe(''); | |
| expect(result.oidc_only).toBe(false); | |
| expect(result.client_secret_set).toBe(false); | |
| expect(result.display_name).toBe(''); | |
| expect(result.discovery_url).toBe(''); | |
| }); | |
| it('ADMIN-SVC-048 β updateOidcSettings persists issuer and client_id, then getOidcSettings returns them', () => { | |
| updateOidcSettings({ issuer: 'https://auth.example.com', client_id: 'my-client' }); | |
| const result = getOidcSettings() as any; | |
| expect(result.issuer).toBe('https://auth.example.com'); | |
| expect(result.client_id).toBe('my-client'); | |
| }); | |
| it('ADMIN-SVC-049 β updateOidcSettings does not write oidc_only (replaced by granular toggles)', () => { | |
| updateOidcSettings({ issuer: 'https://auth.example.com', client_id: 'my-client' }); | |
| const result = getOidcSettings() as any; | |
| // oidc_only is no longer managed by updateOidcSettings; use password_login/oidc_login toggles | |
| expect(result.oidc_only).toBe(false); | |
| }); | |
| }); | |
| // ββ saveDemoBaseline ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('saveDemoBaseline', () => { | |
| afterEach(() => { | |
| vi.unstubAllEnvs(); | |
| }); | |
| it('ADMIN-SVC-050 β returns 404 when DEMO_MODE is not "true"', () => { | |
| vi.stubEnv('DEMO_MODE', 'false'); | |
| const result = saveDemoBaseline() as any; | |
| expect(result.status).toBe(404); | |
| expect(result.error).toBeDefined(); | |
| }); | |
| it('ADMIN-SVC-051 β returns a defined result object when DEMO_MODE is "true"', () => { | |
| // saveDemoBaseline() uses a dynamic CJS require() whose mock cannot be | |
| // intercepted via vi.mock in this test environment (tsx runtime + CJS loader). | |
| // The function either succeeds (message) or falls through the catch to a | |
| // 500 error. Either way the result must be a defined, non-null object. | |
| vi.stubEnv('DEMO_MODE', 'true'); | |
| const result = saveDemoBaseline() as any; | |
| expect(result).toBeDefined(); | |
| expect(typeof result).toBe('object'); | |
| // The 404 branch must NOT be taken β DEMO_MODE is "true". | |
| expect(result.status).not.toBe(404); | |
| }); | |
| }); | |
| // ββ getGithubReleases βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('getGithubReleases', () => { | |
| afterEach(() => { | |
| vi.unstubAllGlobals(); | |
| }); | |
| it('ADMIN-SVC-052 β returns empty array when fetch fails', async () => { | |
| vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Network error'))); | |
| const result = await getGithubReleases(); | |
| expect(Array.isArray(result)).toBe(true); | |
| expect(result).toHaveLength(0); | |
| }); | |
| it('ADMIN-SVC-053 β returns releases array when fetch succeeds', async () => { | |
| const mockReleases = [ | |
| { id: 1, tag_name: 'v3.0.0', name: 'Release 3.0.0', html_url: 'https://github.com/example/releases/tag/v3.0.0' }, | |
| { id: 2, tag_name: 'v2.9.9', name: 'Release 2.9.9', html_url: 'https://github.com/example/releases/tag/v2.9.9' }, | |
| ]; | |
| vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ | |
| ok: true, | |
| json: async () => mockReleases, | |
| })); | |
| const result = await getGithubReleases(); | |
| expect(Array.isArray(result)).toBe(true); | |
| expect(result).toHaveLength(2); | |
| expect((result as any[])[0].tag_name).toBe('v3.0.0'); | |
| }); | |
| }); | |
| // ββ checkVersion ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('checkVersion', () => { | |
| afterEach(() => { | |
| vi.unstubAllGlobals(); | |
| }); | |
| it('ADMIN-SVC-054 β returns update_available:false when fetch fails', async () => { | |
| vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('Network error'))); | |
| const result = await checkVersion() as any; | |
| expect(result.update_available).toBe(false); | |
| expect(result.current).toBeDefined(); | |
| expect(result.latest).toBeDefined(); | |
| }); | |
| it('ADMIN-SVC-055 β returns update_available:true when latest version is greater than current', async () => { | |
| vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ | |
| ok: true, | |
| json: async () => ({ tag_name: 'v999.0.0', html_url: 'https://github.com/example/releases/tag/v999.0.0' }), | |
| })); | |
| const result = await checkVersion() as any; | |
| expect(result.update_available).toBe(true); | |
| expect(result.latest).toBe('999.0.0'); | |
| expect(result.release_url).toBe('https://github.com/example/releases/tag/v999.0.0'); | |
| }); | |
| }); | |
| // ββ getPackingTemplate ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('getPackingTemplate', () => { | |
| it('ADMIN-SVC-056 β returns template with categories and items when template exists', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const tpl = createPackingTemplate('Full Template', admin.id) as any; | |
| const cat = createTemplateCategory(String(tpl.template.id), 'Clothing') as any; | |
| createTemplateItem(String(tpl.template.id), String(cat.category.id), 'T-Shirt'); | |
| const result = getPackingTemplate(String(tpl.template.id)) as any; | |
| expect(result.template).toBeDefined(); | |
| expect(result.template.name).toBe('Full Template'); | |
| expect(Array.isArray(result.categories)).toBe(true); | |
| expect(result.categories.length).toBeGreaterThanOrEqual(1); | |
| expect(Array.isArray(result.items)).toBe(true); | |
| expect(result.items.length).toBeGreaterThanOrEqual(1); | |
| expect(result.items[0].name).toBe('T-Shirt'); | |
| }); | |
| it('ADMIN-SVC-057 β returns 404 for non-existent template', () => { | |
| const result = getPackingTemplate('99999') as any; | |
| expect(result.status).toBe(404); | |
| expect(result.error).toBeDefined(); | |
| }); | |
| }); | |
| // ββ Template items ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('Template items', () => { | |
| it('ADMIN-SVC-058 β createTemplateItem returns item with name', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const tpl = createPackingTemplate('Tpl', admin.id) as any; | |
| const cat = createTemplateCategory(String(tpl.template.id), 'Gear') as any; | |
| const result = createTemplateItem(String(tpl.template.id), String(cat.category.id), 'Backpack') as any; | |
| expect(result.item).toBeDefined(); | |
| expect(result.item.name).toBe('Backpack'); | |
| }); | |
| it('ADMIN-SVC-059 β createTemplateItem returns 400 for empty name', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const tpl = createPackingTemplate('Tpl', admin.id) as any; | |
| const cat = createTemplateCategory(String(tpl.template.id), 'Gear') as any; | |
| const result = createTemplateItem(String(tpl.template.id), String(cat.category.id), '') as any; | |
| expect(result.status).toBe(400); | |
| }); | |
| it('ADMIN-SVC-060 β createTemplateItem returns 404 for non-existent category', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const tpl = createPackingTemplate('Tpl', admin.id) as any; | |
| const result = createTemplateItem(String(tpl.template.id), '99999', 'Item') as any; | |
| expect(result.status).toBe(404); | |
| }); | |
| it('ADMIN-SVC-061 β updateTemplateItem updates name', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const tpl = createPackingTemplate('Tpl', admin.id) as any; | |
| const cat = createTemplateCategory(String(tpl.template.id), 'Gear') as any; | |
| const item = createTemplateItem(String(tpl.template.id), String(cat.category.id), 'Old Item') as any; | |
| const result = updateTemplateItem(String(item.item.id), { name: 'New Item' }) as any; | |
| expect(result.item.name).toBe('New Item'); | |
| }); | |
| it('ADMIN-SVC-062 β updateTemplateItem returns 404 for non-existent item', () => { | |
| const result = updateTemplateItem('99999', { name: 'Ghost' }) as any; | |
| expect(result.status).toBe(404); | |
| }); | |
| it('ADMIN-SVC-063 β deleteTemplateItem removes item', () => { | |
| const { user: admin } = createAdmin(testDb); | |
| const tpl = createPackingTemplate('Tpl', admin.id) as any; | |
| const cat = createTemplateCategory(String(tpl.template.id), 'Gear') as any; | |
| const item = createTemplateItem(String(tpl.template.id), String(cat.category.id), 'To Delete') as any; | |
| const result = deleteTemplateItem(String(item.item.id)) as any; | |
| expect(result.error).toBeUndefined(); | |
| const check = testDb.prepare('SELECT id FROM packing_template_items WHERE id = ?').get(item.item.id); | |
| expect(check).toBeUndefined(); | |
| }); | |
| it('ADMIN-SVC-064 β deleteTemplateItem returns 404 for non-existent item', () => { | |
| const result = deleteTemplateItem('99999') as any; | |
| expect(result.status).toBe(404); | |
| }); | |
| }); | |
| // ββ listAddons ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('listAddons', () => { | |
| it('ADMIN-SVC-065 β listAddons returns array containing seeded addon entries', () => { | |
| const result = listAddons() as any[]; | |
| expect(Array.isArray(result)).toBe(true); | |
| expect(result.length).toBeGreaterThan(0); | |
| const addonIds = result.map((a: any) => a.id); | |
| expect(addonIds).toContain('packing'); | |
| expect(addonIds).toContain('budget'); | |
| }); | |
| }); | |
| // ββ updateAddon βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('updateAddon', () => { | |
| it('ADMIN-SVC-066 β updateAddon enables and disables a seeded addon', () => { | |
| const disabled = updateAddon('mcp', { enabled: false }) as any; | |
| expect(disabled.addon).toBeDefined(); | |
| expect(disabled.addon.enabled).toBe(false); | |
| const enabled = updateAddon('mcp', { enabled: true }) as any; | |
| expect(enabled.addon.enabled).toBe(true); | |
| }); | |
| it('ADMIN-SVC-067 β updateAddon returns 404 for unknown addon id', () => { | |
| const result = updateAddon('nonexistent-addon-xyz', { enabled: true }) as any; | |
| expect(result.status).toBe(404); | |
| expect(result.error).toBeDefined(); | |
| }); | |
| }); | |
| // ββ MCP Tokens ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| describe('MCP Tokens', () => { | |
| it('ADMIN-SVC-068 β listMcpTokens returns empty array initially', () => { | |
| const result = listMcpTokens() as any[]; | |
| expect(Array.isArray(result)).toBe(true); | |
| expect(result).toHaveLength(0); | |
| }); | |
| it('ADMIN-SVC-069 β deleteMcpToken returns 404 for non-existent token', () => { | |
| const result = deleteMcpToken('99999') as any; | |
| expect(result.status).toBe(404); | |
| expect(result.error).toBeDefined(); | |
| }); | |
| }); | |