Files
shd.build/src/routes/isac/protocols/profile-action.test.ts
T

185 lines
5.6 KiB
TypeScript

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<Record<string, unknown>>,
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<void>) => {
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();
});
});