185 lines
5.6 KiB
TypeScript
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();
|
|
});
|
|
});
|