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.
active-svelte/src/arts/aapp/test/ecosystem.integration.test.ts

455 lines
13 KiB

import { describe, expect, it } from 'vitest';
import { createActiveApp } from '../active-app.svelte';
import {
CACHE_EVENT_ALL,
CACHE_EVENT_INVALIDATE,
CACHE_POLICY_INTERACTIVE,
CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
CACHE_SCOPE_TENANT,
type CacheEvent,
type ResolvedScopeValues
} from '$cach';
import { CONNECTION_STATE_CLOSED, CONNECTION_STATE_OPEN, createMockTransport } from '$conn';
import { HTTP_CONTENT_TYPE_JSON, HTTP_HEADER_CONTENT_TYPE } from '$libs/http';
import type { StandardSchemaV1 } from '$libs/standard-schema';
import {
AUTH_AAL,
AUTH_AMR,
AUTH_ROUTE_PATHS,
AUTH_SESSION_STATUSES,
type AuthActorId,
type AuthCurrentView,
type AuthSessionId,
type AuthTenantId
} from '$libs/auth';
import { LogLevel, type LogEntry } from '$logr';
import {
PERMISSION_EFFECT_ALLOW,
PERMISSION_EFFECT_NOT_APPLICABLE,
actor,
allow,
and,
attr,
definePermSchema,
definePolicies,
rel,
type ResourceRef,
type SubjectRef
} from '$perm';
import { createEnginePermissions, createPermissionHttpHandlers } from '$svrs/perm';
import { createMemoryAdapter } from '$stor';
const PERM_ENDPOINT = 'https://ecosystem.test/api/permissions';
const HTTP_PROJECT_PATH = '/api/demo/project';
const TENANT_ID = 'tenant-acme';
const ACTOR_ID = 'actor-ada';
const PROJECT_ID = 'project-atlas';
const PROJECT_ACTION_UPDATE = 'project.update';
const PROJECT_RELATION_MEMBER = 'project.member';
const PROJECT_SCHEMA_VERSION = 'ProjectPayload:v1';
const ROLE_ADMIN = 'admin';
const ROLE_VIEWER = 'viewer';
const TIMER_KEY = 'ecosystem:test';
const CONNECTION_NAME = 'updates';
const LOG_CATEGORY = 'test.ecosystem';
const LOG_MESSAGE_BOOT = 'ecosystem.boot';
const JSON_PASSTHROUGH_SCHEMA: StandardSchemaV1<unknown, unknown> = {
'~standard': {
version: 1,
vendor: 'ecosystem-test',
validate(value) {
return { value };
}
}
};
interface ProjectResource extends ResourceRef {
readonly type: 'project';
readonly id: string;
readonly tenantId: string;
readonly locked: boolean;
}
interface ProjectPayload {
readonly id: string;
readonly tenantId: string;
readonly version: number;
}
interface DemoUser {
readonly id: string;
readonly email: string;
}
interface DemoSessionData {
readonly tenantId: string;
readonly permissionHash: string;
}
describe('ActiveApp — total ecosystem integration', () => {
it('wires auth, sess, perm, cach, http, stor, sium, fmts, fend, adom, timr, conn and logr', async () => {
const entries: LogEntry[] = [];
const authCurrent = createAuthCurrent();
let actorRole = ROLE_ADMIN;
const actorRef = (): SubjectRef => ({
type: 'user',
id: ACTOR_ID,
role: actorRole,
tenantIds: [TENANT_ID]
});
const resource: ProjectResource = {
type: 'project',
id: PROJECT_ID,
tenantId: TENANT_ID,
locked: false
};
const permissionEngine = createPermissionEngine();
const permissionHandlers = createPermissionHttpHandlers(permissionEngine, actorRef);
let projectFetches = 0;
const cacheEvents: CacheEvent[] = [];
const appFetch = (async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const path = requestPath(input);
if (path.startsWith('/api/permissions')) {
const body = init?.body === undefined ? {} : JSON.parse(String(init.body));
const request = {
method: init?.method ?? 'POST',
url: requestUrl(input),
async json() {
return body;
}
};
const handler = path.endsWith('/batch')
? permissionHandlers.batch
: path.endsWith('/what')
? permissionHandlers.what
: path.endsWith('/explain')
? permissionHandlers.explain
: permissionHandlers.check;
const response = await handler(request);
return jsonResponse(response.body, response.status);
}
if (path === AUTH_ROUTE_PATHS.CURRENT) return jsonResponse(authCurrent);
if (path === AUTH_ROUTE_PATHS.CSRF) return jsonResponse({ token: 'csrf-test-token' });
if (path === AUTH_ROUTE_PATHS.SIGN_OUT) return jsonResponse({ ok: true });
if (path === HTTP_PROJECT_PATH) {
projectFetches += 1;
return jsonResponse({ id: PROJECT_ID, tenantId: TENANT_ID, version: projectFetches });
}
return jsonResponse({ error: 'not-found' }, 404);
}) as typeof fetch;
const App = createActiveApp({
lang: {
schema: {
demo: {
title: { es: 'Demo', en: 'Demo' }
}
},
defaultLocale: 'es'
},
logger: {
level: LogLevel.TRACE,
transports: [
{
name: 'capture',
write(entry) {
entries.push(entry);
}
}
]
},
storage: {
adapter: createMemoryAdapter(),
namespace: 'ecosystem-test'
},
http: {
fetch: appFetch,
timeout: 0,
retry: { limit: 0 }
},
cache: {
scopeResolver: (): ResolvedScopeValues => ({
tenantId: TENANT_ID,
actorId: ACTOR_ID,
permissionHash: actorRole,
locale: App.getLocale()
}),
policies: {
[CACHE_POLICY_INTERACTIVE]: {
freshFor: 10_000,
staleFor: 20_000,
staleIfErrorFor: 30_000,
gcAfter: 60_000,
mode: CACHE_READ_MODE_STALE_WHILE_REVALIDATE,
persist: true
}
}
}
});
const offCacheEvents = App.Cache.on(CACHE_EVENT_ALL, (event) => {
cacheEvents.push(event);
});
try {
App.Logger.info(LOG_CATEGORY, LOG_MESSAGE_BOOT);
expect(entries).toHaveLength(1);
expect(App.Lang.t('demo.title')).toBe('Demo');
App.setLocale('ar');
expect(App.Frontend.getDir()).toBe('rtl');
expect(App.Formats.currency.format(1200).length).toBeGreaterThan(0);
expect(App.Dom.resolve({ base: 'mobile', md: 'desktop' })).toBeDefined();
const storageEntry = App.Storage.entry('draft', () => ({ title: 'Atlas' }));
storageEntry.update((draft) => ({ ...draft, title: 'Atlas Prime' }));
expect(storageEntry.current.title).toBe('Atlas Prime');
const Sium = App.createSiumEngine();
const validation = await Sium.validate(
Sium.object({ title: Sium.pipe(Sium.string(), Sium.min(4)) }),
storageEntry.current
);
expect(validation.ok).toBe(true);
const Auth = App.createActiveAuth({ initial: authCurrent });
await Auth.loadCurrent();
expect(Auth.authenticated).toBe(true);
const Sess = App.createActiveSession<DemoUser, undefined, DemoSessionData>({
storage: { adapter: createMemoryAdapter(), key: 'session' }
});
Sess.adoptServer({
user: { id: ACTOR_ID, email: 'ada@acme.test' },
data: { tenantId: TENANT_ID, permissionHash: ROLE_ADMIN },
issuedAt: 1,
expiresAt: Date.now() + 60_000
});
expect(Sess.current?.user?.id).toBe(ACTOR_ID);
const Permissions = App.createActivePermissions({
endpoint: PERM_ENDPOINT,
scopeKey: () => `${ACTOR_ID}:${actorRole}`
});
const decision = await Permissions.check({
action: PROJECT_ACTION_UPDATE,
resource,
context: { risk: { mfa: true } }
});
expect(decision.effect).toBe(PERMISSION_EFFECT_ALLOW);
actorRole = ROLE_VIEWER;
Permissions.invalidate();
const viewerDecision = await Permissions.check({
action: PROJECT_ACTION_UPDATE,
resource,
context: { risk: { mfa: true } }
});
expect(viewerDecision.effect).toBe(PERMISSION_EFFECT_NOT_APPLICABLE);
expect(Permissions.size).toBeGreaterThan(0);
actorRole = ROLE_ADMIN;
Permissions.invalidate();
const project = await App.Cache.query<ProjectPayload>({
key: ['project', PROJECT_ID],
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'project', id: PROJECT_ID }],
fetcher: async () => {
const response = await App.Http.get(HTTP_PROJECT_PATH, {
schema: JSON_PASSTHROUGH_SCHEMA
});
if (!response.ok) throw new Error('project request failed');
return response.value as ProjectPayload;
}
});
const cached = await App.Cache.query<ProjectPayload>({
key: ['project', PROJECT_ID],
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'project', id: PROJECT_ID }],
fetcher: async () => {
throw new Error('cache miss should not call this fetcher');
}
});
expect(project.version).toBe(1);
expect(cached.version).toBe(1);
expect(projectFetches).toBe(1);
App.setLocale('es');
const localizedProject = await App.Cache.query<ProjectPayload>({
key: ['project', PROJECT_ID],
scope: CACHE_SCOPE_TENANT,
policy: CACHE_POLICY_INTERACTIVE,
schemaVersion: PROJECT_SCHEMA_VERSION,
tags: [{ type: 'project', id: PROJECT_ID }],
fetcher: async () => {
const response = await App.Http.get(HTTP_PROJECT_PATH, {
schema: JSON_PASSTHROUGH_SCHEMA
});
if (!response.ok) throw new Error('localized project request failed');
return response.value as ProjectPayload;
}
});
expect(localizedProject.version).toBe(2);
expect(projectFetches).toBe(2);
let timerRan = false;
App.Timers.schedule(TIMER_KEY, 0, () => {
timerRan = true;
});
await new Promise((resolve) => setTimeout(resolve, 0));
expect(timerRan).toBe(true);
const Connections = App.createActiveConnections();
const transport = createMockTransport();
const Updates = Connections.createConnection(CONNECTION_NAME, {
transport,
heartbeat: false,
reconnect: false
});
const connected = await Updates.connect();
expect(connected.ok).toBe(true);
await Updates.send('project.updated', { id: PROJECT_ID });
expect(transport.sentMessages()).toHaveLength(1);
await Auth.signOut();
expect(Auth.authenticated).toBe(false);
expect(Permissions.size).toBe(0);
expect(cacheEvents.some((event) => event.type === CACHE_EVENT_INVALIDATE)).toBe(true);
} finally {
offCacheEvents();
App.dispose();
permissionEngine.dispose();
}
});
it('bridges late-created sessions to existing App connection registries', async () => {
const App = createActiveApp({
logger: { level: LogLevel.NONE, transports: [] }
});
try {
const Connections = App.createActiveConnections();
const transport = createMockTransport();
const Updates = Connections.createConnection(CONNECTION_NAME, {
transport,
heartbeat: false,
reconnect: false,
session: { enabled: true }
});
await Updates.connect();
expect(Updates.state).toBe(CONNECTION_STATE_OPEN);
const Sess = App.createActiveSession<DemoUser>();
Sess.adoptServer({
user: { id: ACTOR_ID, email: 'ada@acme.test' },
issuedAt: 1,
expiresAt: Date.now() + 60_000
});
await Sess.revoke();
expect(Updates.state).toBe(CONNECTION_STATE_CLOSED);
} finally {
App.dispose();
}
});
});
function createPermissionEngine() {
const schema = definePermSchema({
actors: {
user: {
attributes: {
role: 'string',
tenantIds: 'string[]'
}
}
},
resources: {
project: {
actions: ['update'],
attributes: {
tenantId: 'string',
locked: 'boolean'
}
}
},
relations: {
[PROJECT_RELATION_MEMBER]: { from: 'project', to: 'user' }
},
context: {
risk: { mfa: 'boolean' }
}
});
return createEnginePermissions({
schema,
policies: definePolicies(schema, [
allow(PROJECT_ACTION_UPDATE).when(
and(
rel(PROJECT_RELATION_MEMBER).is(actor()),
attr('actor.role').eq(ROLE_ADMIN),
attr('project.locked').eq(false),
attr('context.risk.mfa').eq(true)
)
)
]),
providers: {
relations: {
hasRelation({ relation, resource, subject }) {
if (relation !== PROJECT_RELATION_MEMBER) return 'unknown';
expect(subject.id).toBe(ACTOR_ID);
return Array.isArray(subject.tenantIds) && subject.tenantIds.includes(resource.tenantId);
}
}
}
});
}
function createAuthCurrent(): AuthCurrentView {
return {
session: {
status: AUTH_SESSION_STATUSES.AUTHENTICATED,
actorRef: {
tenantId: TENANT_ID as AuthTenantId,
actorId: ACTOR_ID as AuthActorId
},
sessionId: 'sess-test' as AuthSessionId,
aal: AUTH_AAL.MULTI_FACTOR,
amr: [AUTH_AMR.PASSWORD, AUTH_AMR.TOTP],
authTime: Date.now(),
expiresAt: Date.now() + 60_000
},
actor: {
tenantId: TENANT_ID as AuthTenantId,
actorId: ACTOR_ID as AuthActorId,
displayName: 'Ada',
primaryIdentifier: 'ada@acme.test'
}
};
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { [HTTP_HEADER_CONTENT_TYPE]: HTTP_CONTENT_TYPE_JSON }
});
}
function requestUrl(input: RequestInfo | URL): string {
if (typeof input === 'string') return input;
if (input instanceof URL) return input.href;
return input.url;
}
function requestPath(input: RequestInfo | URL): string {
const url = requestUrl(input);
if (url.startsWith('http://') || url.startsWith('https://')) return new URL(url).pathname;
return url.split('?')[0] ?? url;
}

Powered by TurnKey Linux.