Files
cinny/src/app/utils/outbox.test.ts
T

132 lines
4.6 KiB
TypeScript
Raw Normal View History

import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
OUTBOX_AUTOSEND_MS,
OUTBOX_MAX_ENTRIES,
OutboxEntry,
isRetryableSendError,
outboxRetryDelayMs,
parseOutbox,
planRestore,
shouldKeepInOutbox,
withEntry,
withoutEntry,
} from './outbox';
const entry = (txnId: string, ts: number, roomId = '!a:hs'): OutboxEntry => ({
txnId,
roomId,
threadId: null,
type: 'm.room.message',
content: { msgtype: 'm.text', body: txnId },
ts,
});
test('keeps message-like events, not call signalling or redactions', () => {
assert.equal(shouldKeepInOutbox('m.room.message', { body: 'hi' }), true);
assert.equal(shouldKeepInOutbox('m.reaction', { 'm.relates_to': { event_id: '$x' } }), true);
assert.equal(shouldKeepInOutbox('org.matrix.msc3381.poll.start', {}), true);
assert.equal(shouldKeepInOutbox('org.matrix.msc4075.rtc.notification', {}), false);
assert.equal(shouldKeepInOutbox('m.call.invite', {}), false);
assert.equal(shouldKeepInOutbox('m.room.redaction', {}), false);
});
test('skips events that point at another unsent message (local id)', () => {
assert.equal(
shouldKeepInOutbox('m.reaction', { 'm.relates_to': { event_id: '~!a:hs:m123' } }),
false,
);
assert.equal(
shouldKeepInOutbox('m.room.message', {
body: 'reply',
'm.relates_to': { 'm.in_reply_to': { event_id: '~!a:hs:m1' } },
}),
false,
);
assert.equal(
shouldKeepInOutbox('m.room.message', {
body: 'reply',
'm.relates_to': { 'm.in_reply_to': { event_id: '$real' } },
}),
true,
);
});
test('retryable: connection loss, timeouts, rate limits, server errors', () => {
const connectionError = new Error('fetch failed');
Object.defineProperty(connectionError, 'name', { value: 'ConnectionError' });
assert.equal(isRetryableSendError(connectionError), true);
assert.equal(isRetryableSendError({ httpStatus: 429 }), true);
assert.equal(isRetryableSendError({ httpStatus: 408 }), true);
assert.equal(isRetryableSendError({ httpStatus: 502 }), true);
});
test('not retryable: client errors, consent, unknown or missing errors', () => {
assert.equal(isRetryableSendError({ httpStatus: 403, errcode: 'M_FORBIDDEN' }), false);
assert.equal(isRetryableSendError({ httpStatus: 403, errcode: 'M_CONSENT_NOT_GIVEN' }), false);
assert.equal(isRetryableSendError({ httpStatus: 400 }), false);
assert.equal(isRetryableSendError(new Error('encryption failed')), false);
assert.equal(isRetryableSendError(undefined), false);
assert.equal(isRetryableSendError(null), false);
});
test('parse: only this user, only well-formed entries, junk tolerated', () => {
const good = entry('m1', 1);
const raw = JSON.stringify({ userId: '@me:hs', entries: [good, { txnId: 5 }, null] });
assert.deepEqual(parseOutbox(raw, '@me:hs'), [good]);
assert.deepEqual(parseOutbox(raw, '@other:hs'), []);
assert.deepEqual(parseOutbox('{not json', '@me:hs'), []);
assert.deepEqual(parseOutbox(null, '@me:hs'), []);
});
test('withEntry: first write wins, sorted, capped (oldest dropped)', () => {
const a = entry('a', 2);
let list = withEntry([], a);
assert.equal(withEntry(list, { ...a, content: { body: 'changed' } }), list);
list = withEntry(list, entry('b', 1));
assert.deepEqual(
list.map((e) => e.txnId),
['b', 'a'],
);
let many: OutboxEntry[] = [];
for (let i = 0; i < OUTBOX_MAX_ENTRIES + 5; i += 1) many = withEntry(many, entry(`t${i}`, i));
assert.equal(many.length, OUTBOX_MAX_ENTRIES);
assert.equal(many[0].txnId, 't5');
});
test('withoutEntry: removes, and returns the same list when absent', () => {
const list = [entry('a', 1), entry('b', 2)];
assert.deepEqual(
withoutEntry(list, 'a').map((e) => e.txnId),
['b'],
);
assert.equal(withoutEntry(list, 'zzz'), list);
});
test('restore plan: drops delivered / left rooms, auto-sends only recent ones', () => {
const now = 10 * OUTBOX_AUTOSEND_MS;
const recent = entry('recent', now - 60_000);
const old = entry('old', now - OUTBOX_AUTOSEND_MS - 1);
const delivered = entry('delivered', now - 1000);
const left = entry('left', now - 1000, '!left:hs');
const plan = planRestore(
[recent, left, old, delivered],
now,
(roomId) => roomId !== '!left:hs',
(e) => e.txnId === 'delivered',
);
assert.deepEqual(
plan.restore.map((e) => e.txnId),
['old', 'recent'],
);
assert.deepEqual([...plan.autoSend], ['recent']);
assert.deepEqual(plan.drop.map((e) => e.txnId).sort(), ['delivered', 'left']);
});
test('blip retry delay backs off and is capped at a minute', () => {
assert.deepEqual(
[0, 1, 2, 3, 4, 9].map(outboxRetryDelayMs),
[5000, 10000, 20000, 40000, 60000, 60000],
);
});