You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
311 lines
8.9 KiB
311 lines
8.9 KiB
/**
|
|
* End-to-end tests for the orca-based presets in
|
|
* `arts/active-app/presets/`. Validate that:
|
|
* - Publishing a SESSION_EVENT_* on the bus triggers the registered
|
|
* orca action.
|
|
* - The action calls the imperative API on the relevant service.
|
|
* - Detachers actually unregister the action.
|
|
* - The aggregator only registers presets whose services are present.
|
|
*/
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
applyCacheClearOnIdentityChange,
|
|
applyCacheClearOnRevoke,
|
|
applyConnectionsCloseOnRevoke,
|
|
applyConnectionsReauthOnIdentityChange,
|
|
applyPermInvalidateOnIdentityChange,
|
|
applyStandardOrca
|
|
} from '../presets/index.ts';
|
|
import { createSvelteEngineBus } from '$bus';
|
|
import { createEngineLogger } from '$logger/engine-logger';
|
|
import { createEngineOrca, type EngineOrca } from '$orca';
|
|
import { createActiveTimers } from '$timer/active-timers.svelte';
|
|
import { SESSION_EVENT_IDENTITY_CHANGED, SESSION_EVENT_REVOKED } from '$session';
|
|
import type { EngineLogger } from '$logger';
|
|
import type { EngineBus } from '$bus';
|
|
import type { ActiveTimers } from '$timer';
|
|
import type { ActiveCache } from '$cache/types';
|
|
import type { ActiveConnections } from '$connection/types';
|
|
import type { ActivePerms } from '$perm/types';
|
|
|
|
interface CoreState {
|
|
logger: EngineLogger;
|
|
bus: EngineBus<Record<string, unknown>>;
|
|
timers: ActiveTimers;
|
|
orca: EngineOrca;
|
|
}
|
|
|
|
function buildCore(): CoreState {
|
|
const logger = createEngineLogger({});
|
|
const timers = createActiveTimers({ logger });
|
|
const bus = createSvelteEngineBus<Record<string, unknown>>({ logger, clock: timers.clock });
|
|
const orca = createEngineOrca({ bus, timers, logger });
|
|
return { logger, bus, timers, orca };
|
|
}
|
|
|
|
function disposeCore(core: CoreState): void {
|
|
core.orca.dispose();
|
|
core.bus.dispose();
|
|
core.timers.dispose();
|
|
core.logger.dispose();
|
|
}
|
|
|
|
const samplePayload = {
|
|
event: 'session.lifecycle.adopted',
|
|
generation: 1,
|
|
identity: { from: 'anon', to: 'user-42' },
|
|
previousActorId: null,
|
|
nextActorId: 'user-42'
|
|
};
|
|
|
|
async function flush(rounds = 4) {
|
|
for (let i = 0; i < rounds; i++) {
|
|
await new Promise((r) => queueMicrotask(() => r(undefined)));
|
|
await new Promise((r) => setTimeout(r, 0));
|
|
}
|
|
}
|
|
|
|
describe('applyCacheClearOnIdentityChange', () => {
|
|
let core: CoreState;
|
|
|
|
beforeEach(() => {
|
|
core = buildCore();
|
|
});
|
|
afterEach(() => {
|
|
disposeCore(core);
|
|
});
|
|
|
|
it('clears the cache when SESSION_EVENT_IDENTITY_CHANGED is published', async () => {
|
|
const clear = vi.fn(() => Promise.resolve());
|
|
const cache = { clear } as unknown as ActiveCache;
|
|
|
|
applyCacheClearOnIdentityChange({ orca: core.orca, cache });
|
|
|
|
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
|
await flush();
|
|
|
|
expect(clear).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('detacher unregisters the action', async () => {
|
|
const clear = vi.fn(() => Promise.resolve());
|
|
const cache = { clear } as unknown as ActiveCache;
|
|
|
|
const detach = applyCacheClearOnIdentityChange({ orca: core.orca, cache });
|
|
detach();
|
|
|
|
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
|
await flush();
|
|
|
|
expect(clear).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('records action failure in run trace when clear() rejects', async () => {
|
|
const error = new Error('clear failed');
|
|
const cache = { clear: vi.fn(() => Promise.reject(error)) } as unknown as ActiveCache;
|
|
|
|
applyCacheClearOnIdentityChange({ orca: core.orca, cache });
|
|
|
|
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
|
await flush();
|
|
|
|
const runs = core.orca.recentRuns();
|
|
expect(runs).toHaveLength(1);
|
|
expect(runs[0].actions[0].status).toBe('error');
|
|
expect(runs[0].actions[0].error).toBe(error);
|
|
});
|
|
});
|
|
|
|
describe('applyPermInvalidateOnIdentityChange', () => {
|
|
let core: CoreState;
|
|
|
|
beforeEach(() => {
|
|
core = buildCore();
|
|
});
|
|
afterEach(() => {
|
|
disposeCore(core);
|
|
});
|
|
|
|
it('invalidates perms when SESSION_EVENT_IDENTITY_CHANGED is published', async () => {
|
|
const invalidate = vi.fn();
|
|
const perm = { invalidate } as unknown as ActivePerms;
|
|
|
|
applyPermInvalidateOnIdentityChange({ orca: core.orca, perm });
|
|
|
|
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
|
await flush();
|
|
|
|
expect(invalidate).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('applyCacheClearOnRevoke', () => {
|
|
let core: CoreState;
|
|
|
|
beforeEach(() => {
|
|
core = buildCore();
|
|
});
|
|
afterEach(() => {
|
|
disposeCore(core);
|
|
});
|
|
|
|
it('clears the cache when SESSION_EVENT_REVOKED is published', async () => {
|
|
const clear = vi.fn(() => Promise.resolve());
|
|
const cache = { clear } as unknown as ActiveCache;
|
|
|
|
applyCacheClearOnRevoke({ orca: core.orca, cache });
|
|
|
|
const revokePayload = {
|
|
...samplePayload,
|
|
event: 'session.lifecycle.revoked'
|
|
};
|
|
core.bus.publish(SESSION_EVENT_REVOKED, revokePayload);
|
|
await flush();
|
|
|
|
expect(clear).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('applyConnectionsReauthOnIdentityChange', () => {
|
|
let core: CoreState;
|
|
|
|
beforeEach(() => {
|
|
core = buildCore();
|
|
});
|
|
afterEach(() => {
|
|
disposeCore(core);
|
|
});
|
|
|
|
it('reauthenticates connections when SESSION_EVENT_IDENTITY_CHANGED is published', async () => {
|
|
const reauthenticateAll = vi.fn(() => Promise.resolve([]));
|
|
const connections = { reauthenticateAll } as unknown as ActiveConnections;
|
|
|
|
applyConnectionsReauthOnIdentityChange({ orca: core.orca, connections });
|
|
|
|
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
|
await flush();
|
|
|
|
expect(reauthenticateAll).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('records action failure in run trace when reauthenticateAll() rejects', async () => {
|
|
const error = new Error('reauth failed');
|
|
const connections = {
|
|
reauthenticateAll: vi.fn(() => Promise.reject(error))
|
|
} as unknown as ActiveConnections;
|
|
|
|
applyConnectionsReauthOnIdentityChange({ orca: core.orca, connections });
|
|
|
|
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
|
await flush();
|
|
|
|
const runs = core.orca.recentRuns();
|
|
expect(runs).toHaveLength(1);
|
|
expect(runs[0].actions[0].status).toBe('error');
|
|
expect(runs[0].actions[0].error).toBe(error);
|
|
});
|
|
});
|
|
|
|
describe('applyConnectionsCloseOnRevoke', () => {
|
|
let core: CoreState;
|
|
|
|
beforeEach(() => {
|
|
core = buildCore();
|
|
});
|
|
afterEach(() => {
|
|
disposeCore(core);
|
|
});
|
|
|
|
it('closes connections when SESSION_EVENT_REVOKED is published', async () => {
|
|
const closeAll = vi.fn();
|
|
const connections = { closeAll } as unknown as ActiveConnections;
|
|
|
|
applyConnectionsCloseOnRevoke({ orca: core.orca, connections });
|
|
|
|
const revokePayload = {
|
|
...samplePayload,
|
|
event: 'session.lifecycle.revoked'
|
|
};
|
|
core.bus.publish(SESSION_EVENT_REVOKED, revokePayload);
|
|
await flush();
|
|
|
|
expect(closeAll).toHaveBeenCalledTimes(1);
|
|
expect(closeAll).toHaveBeenCalledWith('session-revoked');
|
|
});
|
|
});
|
|
|
|
describe('applyStandardOrca', () => {
|
|
let core: CoreState;
|
|
|
|
beforeEach(() => {
|
|
core = buildCore();
|
|
});
|
|
afterEach(() => {
|
|
disposeCore(core);
|
|
});
|
|
|
|
it('registers cache + perm reactions when both services are present', async () => {
|
|
const cacheClear = vi.fn(() => Promise.resolve());
|
|
const permInvalidate = vi.fn();
|
|
const cache = { clear: cacheClear } as unknown as ActiveCache;
|
|
const perm = { invalidate: permInvalidate } as unknown as ActivePerms;
|
|
|
|
applyStandardOrca({ orca: core.orca, cache, perm });
|
|
|
|
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
|
await flush();
|
|
|
|
expect(cacheClear).toHaveBeenCalledTimes(1);
|
|
expect(permInvalidate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('registers connection reauth + close when connections is present', async () => {
|
|
const reauthenticateAll = vi.fn(() => Promise.resolve([]));
|
|
const closeAll = vi.fn();
|
|
const connections = { reauthenticateAll, closeAll } as unknown as ActiveConnections;
|
|
|
|
applyStandardOrca({ orca: core.orca, connections });
|
|
|
|
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
|
await flush();
|
|
expect(reauthenticateAll).toHaveBeenCalledTimes(1);
|
|
|
|
core.bus.publish(SESSION_EVENT_REVOKED, {
|
|
...samplePayload,
|
|
event: 'session.lifecycle.revoked'
|
|
});
|
|
await flush();
|
|
expect(closeAll).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('skips cache reactions when cache is absent', async () => {
|
|
const permInvalidate = vi.fn();
|
|
const perm = { invalidate: permInvalidate } as unknown as ActivePerms;
|
|
|
|
applyStandardOrca({ orca: core.orca, perm });
|
|
|
|
// No cache action registered -> no error from publish
|
|
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
|
await flush();
|
|
|
|
expect(permInvalidate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('detach removes every registered preset', async () => {
|
|
const cacheClear = vi.fn(() => Promise.resolve());
|
|
const permInvalidate = vi.fn();
|
|
const cache = { clear: cacheClear } as unknown as ActiveCache;
|
|
const perm = { invalidate: permInvalidate } as unknown as ActivePerms;
|
|
|
|
const detach = applyStandardOrca({ orca: core.orca, cache, perm });
|
|
detach();
|
|
|
|
core.bus.publish(SESSION_EVENT_IDENTITY_CHANGED, samplePayload);
|
|
await flush();
|
|
|
|
expect(cacheClear).not.toHaveBeenCalled();
|
|
expect(permInvalidate).not.toHaveBeenCalled();
|
|
});
|
|
});
|