import { beforeEach, describe, expect, it, vi } from 'vitest'; const mocks = vi.hoisted(() => { const transaction = { prepare: vi.fn() }; const database = { prepare: vi.fn(), transactionAsync: vi.fn(), }; return { transaction, database, profileExists: false, effects: [] as Array>, getAttributeIds: vi.fn(), recordIsacAudit: vi.fn(), }; }); vi.mock('$lib/server/auth/session', () => ({ getSessionUser: vi.fn(async () => ({ id: 'operator-1' })), })); vi.mock('$lib/server/auth/authorization', () => ({ requireDataView: vi.fn(async (user) => user), requirePermission: vi.fn(async (user) => user), })); vi.mock('$lib/server/database', () => ({ getDatabase: vi.fn(async () => mocks.database), })); vi.mock('$lib/server/audit', () => ({ auditRecord: vi.fn(), recordIsacAudit: mocks.recordIsacAudit, })); vi.mock('$lib/server/data-moderation', () => ({ getPendingEntities: vi.fn(async () => []), })); vi.mock('$lib/server/os-protocol-profiles', () => ({ getOsProtocolProfileAttributeIds: mocks.getAttributeIds, })); vi.mock('$lib/server/game-data-icons', () => ({ applyIconMutation: vi.fn(), gameDataIconMetadata: vi.fn(), gameDataIconUrl: vi.fn(() => ''), GameDataIconValidationError: class extends Error {}, readIconMutation: vi.fn(), })); vi.mock('$lib/server/managed-images', () => ({ applyGameDataRender: vi.fn(), ManagedImageValidationError: class extends Error {}, managedImageUrl: vi.fn(() => ''), processManagedImage: vi.fn(), readGameDataRenderMutation: vi.fn(), })); import { actions } from './+page.server'; const request = (includeValues = true) => { const form = new FormData(); form.set('quality', 'Standard'); form.set('attributeType', 'Firepower'); if (includeValues) { form.set('valueCore', '10'); form.set('valueDamage', '20'); form.set('valueHealth', '30'); } return new Request('http://localhost/isac/protocols?/saveProfile', { method: 'POST', body: form, }); }; describe('OS Protocol shared profile action', () => { beforeEach(() => { vi.clearAllMocks(); mocks.profileExists = false; mocks.effects = []; mocks.getAttributeIds.mockResolvedValue({ Core: 'firepower-flat', Damage: 'weapon-damage-flat', Health: 'health-flat', }); mocks.transaction.prepare.mockImplementation(async (sql: string) => ({ get: vi.fn(async () => { if (sql.includes('WHERE quality=?')) return mocks.profileExists ? { id: 'profile-1' } : null; if (sql.includes('WHERE id=?')) return mocks.profileExists ? { id: 'profile-1', quality: 'Standard', primary_attribute_type: 'Firepower', } : null; return null; }), all: vi.fn(async () => sql.includes('os_protocol_secondary_effects') ? mocks.effects : [], ), run: vi.fn(async (...args: unknown[]) => { if (sql.includes('INSERT INTO os_protocol_secondary_profiles')) mocks.profileExists = true; if (sql.includes('DELETE FROM os_protocol_secondary_effects')) mocks.effects = []; if (sql.includes('INSERT INTO os_protocol_secondary_effects')) mocks.effects.push({ profile_id: 'profile-1', effect_role: args[2], stat_attribute_id: args[3], value: args[4], }); }), })); mocks.database.transactionAsync.mockImplementation( (callback: (transaction: typeof mocks.transaction) => Promise) => { const run = vi.fn(() => callback(mocks.transaction)); return Object.assign(run, { immediate: run }); }, ); }); it('creates and audits all three profile roles in one owned transaction', async () => { const result = await actions.saveProfile?.({ cookies: {} as never, request: request(), } as never); expect(result).toMatchObject({ profileSaved: true, resultToken: expect.any(String), }); expect(mocks.database.transactionAsync).toHaveBeenCalledOnce(); expect(mocks.effects).toHaveLength(3); expect(mocks.recordIsacAudit).toHaveBeenCalledWith( expect.objectContaining({ actorId: 'operator-1', category: 'game_data', action: 'os_protocol_profile.created', before: null, after: expect.objectContaining({ effects: expect.arrayContaining([ expect.objectContaining({ effect_role: 'Core', value: 10 }), expect.objectContaining({ effect_role: 'Damage', value: 20 }), expect.objectContaining({ effect_role: 'Health', value: 30 }), ]), }), database: mocks.transaction, }), ); }); it('does not start persistence when a required Attribute is missing', async () => { mocks.getAttributeIds.mockResolvedValue({ Core: 'firepower-flat', Damage: null, Health: 'health-flat', }); const result = await actions.saveProfile?.({ cookies: {} as never, request: request(), } as never); expect(result).toMatchObject({ status: 400 }); expect(mocks.database.transactionAsync).not.toHaveBeenCalled(); expect(mocks.recordIsacAudit).not.toHaveBeenCalled(); }); it('rejects missing numeric values instead of coercing them to zero', async () => { const result = await actions.saveProfile?.({ cookies: {} as never, request: request(false), } as never); expect(result).toMatchObject({ status: 400 }); expect(mocks.database.transactionAsync).not.toHaveBeenCalled(); expect(mocks.recordIsacAudit).not.toHaveBeenCalled(); }); });