Files

155 lines
6.1 KiB
TypeScript
Raw Permalink Normal View History

import { describe, test, expect } from 'bun:test';
import { createObserver, StateAggregator } from '../src/index.js';
import { SubtleCryptoProvider, MemoryStorage } from '@shade/crypto-web';
import { ShadeSessionManager, ShadeEventEmitter } from '@shade/core';
import { PrekeyServerEvents } from '@shade/server';
const crypto = new SubtleCryptoProvider();
const TEST_TOKEN = 'test-token-must-be-at-least-16-chars';
describe('StateAggregator', () => {
test('aggregates client events into snapshot', async () => {
const events = new ShadeEventEmitter();
const agg = new StateAggregator(events);
const alice = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
await alice.initialize();
await alice.generateOneTimePreKeys(10);
const snap = agg.snapshot();
expect(snap.identity.fingerprint).toBeTruthy();
expect(snap.identity.registrationId).toBeGreaterThan(0);
expect(snap.prekeys.oneTimeRemaining).toBe(10);
expect(snap.prekeys.lastGenerated).toBeGreaterThan(0);
});
test('tracks sessions across encrypt/decrypt', async () => {
const events = new ShadeEventEmitter();
const agg = new StateAggregator(events);
const alice = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
const bob = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
await alice.initialize();
await bob.initialize();
const otpks = await bob.generateOneTimePreKeys(5);
const bundle = await bob.createPreKeyBundle();
bundle.oneTimePreKey = { keyId: otpks[0].keyId, publicKey: otpks[0].keyPair.publicKey };
await alice.initSessionFromBundle('bob', bundle);
const env = await alice.encrypt('bob', 'hello');
await bob.decrypt('alice', env);
const snap = agg.snapshot();
const aliceToBob = snap.sessions.find((s) => s.address === 'bob');
expect(aliceToBob).toBeDefined();
expect(aliceToBob!.messageCountSent).toBe(1);
});
test('tracks server events', () => {
const serverEvents = new PrekeyServerEvents();
const agg = new StateAggregator(undefined, serverEvents);
serverEvents.emit('server.identity_registered', { address: 'alice', identityKeyHash: 'abc' });
serverEvents.emit('server.identity_registered', { address: 'bob', identityKeyHash: 'def' });
serverEvents.emit('server.bundle_fetched', { address: 'alice', hadOneTimePreKey: true });
serverEvents.emit('server.bundle_fetched', { address: 'alice', hadOneTimePreKey: false });
serverEvents.emit('server.identity_deleted', { address: 'alice' });
const snap = agg.snapshot();
expect(snap.server.registeredIdentities.has('bob')).toBe(true);
expect(snap.server.registeredIdentities.has('alice')).toBe(false);
expect(snap.server.totalBundleFetches).toBe(2);
expect(snap.server.totalDeleted).toBe(1);
});
});
describe('Observer routes', () => {
test('refuses requests without token', async () => {
const events = new ShadeEventEmitter();
const observer = createObserver({ token: TEST_TOKEN, clientEvents: events });
const res = await observer.request('/api/state');
expect(res.status).toBe(401);
});
test('accepts requests with valid bearer token', async () => {
const events = new ShadeEventEmitter();
const observer = createObserver({ token: TEST_TOKEN, clientEvents: events });
const mgr = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
await mgr.initialize();
const res = await observer.request('/api/state', {
headers: { Authorization: `Bearer ${TEST_TOKEN}` },
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.identity.fingerprint).toBeTruthy();
});
test('refuses requests with wrong token', async () => {
const events = new ShadeEventEmitter();
const observer = createObserver({ token: TEST_TOKEN, clientEvents: events });
const res = await observer.request('/api/state', {
headers: { Authorization: 'Bearer wrong-token-also-long-enough' },
});
expect(res.status).toBe(401);
});
test('accepts token via query string for SSE', async () => {
const events = new ShadeEventEmitter();
const observer = createObserver({ token: TEST_TOKEN, clientEvents: events });
// Just check that the auth middleware accepts the query token
const res = await observer.request(`/api/state?token=${TEST_TOKEN}`);
expect(res.status).toBe(200);
});
test('refuses startup with too-short token', () => {
expect(() => createObserver({ token: 'short' })).toThrow();
});
test('health endpoint works without auth', async () => {
const observer = createObserver({ token: TEST_TOKEN });
const res = await observer.request('/health');
expect(res.status).toBe(200);
const body = await res.json();
expect(body.status).toBe('ok');
});
test('snapshot reflects state after operations', async () => {
const events = new ShadeEventEmitter();
const observer = createObserver({ token: TEST_TOKEN, clientEvents: events });
const alice = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
const bob = new ShadeSessionManager(crypto, new MemoryStorage(), { events });
await alice.initialize();
await bob.initialize();
const otpks = await bob.generateOneTimePreKeys(3);
const bundle = await bob.createPreKeyBundle();
bundle.oneTimePreKey = { keyId: otpks[0].keyId, publicKey: otpks[0].keyPair.publicKey };
await alice.initSessionFromBundle('bob', bundle);
const env = await alice.encrypt('bob', 'hi');
await bob.decrypt('alice', env);
const res = await observer.request('/api/state', {
headers: { Authorization: `Bearer ${TEST_TOKEN}` },
});
const body = await res.json();
expect(body.sessions.length).toBeGreaterThan(0);
// Bob started with 3 OTPKs; Alice consumed one via X3DH PreKeyMessage decrypt
expect(body.prekeys.oneTimeRemaining).toBe(2);
});
test('placeholder dashboard renders when no dist', async () => {
const observer = createObserver({ token: TEST_TOKEN });
const res = await observer.request('/dashboard/');
expect(res.status).toBe(200);
const html = await res.text();
expect(html).toContain('Shade Observer');
});
});