Files
Oxicloud/frontend/src/lib/message-bus/frames.test.ts
T
2026-09-11 00:59:49 +02:00

108 lines
3.2 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { parseIncoming, pingFrame, subscribeFrame, unsubscribeFrame } from './frames';
describe('frame builders', () => {
it('subscribeFrame produces a valid JSON-RPC 2.0 request', () => {
expect(subscribeFrame(7, 'folder:abc')).toEqual({
jsonrpc: '2.0',
id: 7,
method: 'rt.subscribe',
params: { topic: 'folder:abc' }
});
});
it('unsubscribeFrame mirrors the subscribe shape', () => {
expect(unsubscribeFrame(8, 'folder:abc')).toEqual({
jsonrpc: '2.0',
id: 8,
method: 'rt.unsubscribe',
params: { topic: 'folder:abc' }
});
});
it('pingFrame omits params entirely (matches the wire spec)', () => {
const frame = pingFrame(9);
expect(frame).toEqual({ jsonrpc: '2.0', id: 9, method: 'rt.ping' });
expect('params' in frame).toBe(false);
});
});
describe('parseIncoming', () => {
it('recognises an `rt.event` notification', () => {
const raw = JSON.stringify({
jsonrpc: '2.0',
method: 'rt.event',
params: {
topic: 'folder:abc',
event: 'file_created',
data: { file_id: 'x', name: 'a.txt', parent_id: 'abc', actor: 'me' }
}
});
const result = parseIncoming(raw);
expect(result.kind).toBe('event');
if (result.kind === 'event') {
expect(result.params.topic).toBe('folder:abc');
expect(result.params.event).toBe('file_created');
}
});
it('recognises an `rt.revoked` notification', () => {
const raw = JSON.stringify({
jsonrpc: '2.0',
method: 'rt.revoked',
params: { topic: 'folder:abc', reason: 'grant_revoked' }
});
const result = parseIncoming(raw);
expect(result.kind).toBe('revoked');
if (result.kind === 'revoked') expect(result.params.topic).toBe('folder:abc');
});
it('recognises a success response', () => {
const raw = JSON.stringify({
jsonrpc: '2.0',
id: 42,
result: { subscribed: 'folder:abc' }
});
const result = parseIncoming(raw);
expect(result.kind).toBe('success');
if (result.kind === 'success') {
expect(result.id).toBe(42);
expect(result.result).toEqual({ subscribed: 'folder:abc' });
}
});
it('recognises an error response and preserves the code', () => {
const raw = JSON.stringify({
jsonrpc: '2.0',
id: 42,
error: { code: -32001, message: 'no_read', data: { topic: 'folder:xyz' } }
});
const result = parseIncoming(raw);
expect(result.kind).toBe('error');
if (result.kind === 'error') {
expect(result.id).toBe(42);
expect(result.error.code).toBe(-32001);
expect(result.error.message).toBe('no_read');
}
});
it('collapses malformed frames to `ignore` with a stable reason key', () => {
expect(parseIncoming('not-json').kind).toBe('ignore');
expect(parseIncoming('[]').kind).toBe('ignore');
expect(parseIncoming(JSON.stringify({ jsonrpc: '1.0', method: 'rt.event' })).kind).toBe(
'ignore'
);
expect(parseIncoming(JSON.stringify({ jsonrpc: '2.0', method: 'rt.unknown' })).kind).toBe(
'ignore'
);
});
it('never throws — always returns a discriminated result', () => {
// Random shapes that used to trigger throws in earlier drafts.
const cases: string[] = ['', 'null', '42', '{}', '{"jsonrpc":"2.0"}'];
for (const c of cases) {
expect(() => parseIncoming(c)).not.toThrow();
}
});
});