feat: expand ISAC content modeling
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
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(),
|
||||
}));
|
||||
|
||||
vi.mock('$lib/server/conditional-effects', () => ({
|
||||
conditionalRuleSnapshot: vi.fn(),
|
||||
loadConditionalRuleDraft: vi.fn(),
|
||||
parseConditionalRulePayload: vi.fn(),
|
||||
saveConditionalRuleInTransaction: 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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user