134 lines
4.1 KiB
TypeScript
134 lines
4.1 KiB
TypeScript
import { test } from 'node:test';
|
|||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import { MatrixClient, MatrixError } from 'matrix-js-sdk';
|
||
|
|
import {
|
||
|
|
collectOwnEventIds,
|
||
|
|
isRedactableOwnEvent,
|
||
|
|
ownMessagesFilter,
|
||
|
|
redactPending,
|
||
|
|
RedactJobState,
|
||
|
|
} from './redactOwnMessages';
|
||
|
|
|
||
|
|
(globalThis as { localStorage?: unknown }).localStorage = {
|
||
|
|
getItem: () => null,
|
||
|
|
setItem: () => undefined,
|
||
|
|
removeItem: () => undefined,
|
||
|
|
};
|
||
|
|
|
||
|
|
test('state events, redactions and already-redacted events are not candidates', () => {
|
||
|
|
assert.equal(isRedactableOwnEvent({ type: 'm.room.message', content: { body: 'x' } }), true);
|
||
|
|
assert.equal(isRedactableOwnEvent({ type: 'm.reaction', content: { 'm.relates_to': {} } }), true);
|
||
|
|
assert.equal(
|
||
|
|
isRedactableOwnEvent({
|
||
|
|
type: 'm.room.member',
|
||
|
|
state_key: '@me',
|
||
|
|
content: { membership: 'join' },
|
||
|
|
}),
|
||
|
|
false,
|
||
|
|
);
|
||
|
|
assert.equal(isRedactableOwnEvent({ type: 'm.room.redaction', content: {} }), false);
|
||
|
|
assert.equal(
|
||
|
|
isRedactableOwnEvent({
|
||
|
|
type: 'm.room.message',
|
||
|
|
content: {},
|
||
|
|
unsigned: { redacted_because: {} },
|
||
|
|
}),
|
||
|
|
false,
|
||
|
|
);
|
||
|
|
assert.equal(isRedactableOwnEvent({ type: 'm.room.message', content: {} }), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('the server-side filter asks only for my events', () => {
|
||
|
|
const f = ownMessagesFilter('@me:hs');
|
||
|
|
assert.deepEqual(f.getRoomTimelineFilterComponent()?.toJSON(), { senders: ['@me:hs'] });
|
||
|
|
});
|
||
|
|
|
||
|
|
test('collect paginates until the end token stops moving and keeps only my redactable events', async () => {
|
||
|
|
const pages: Record<string, { chunk: unknown[]; end?: string }> = {
|
||
|
|
start: {
|
||
|
|
chunk: [
|
||
|
|
{ event_id: '$1', sender: '@me:hs', type: 'm.room.message', content: { body: 'a' } },
|
||
|
|
{ event_id: '$2', sender: '@other:hs', type: 'm.room.message', content: { body: 'b' } },
|
||
|
|
],
|
||
|
|
end: 't1',
|
||
|
|
},
|
||
|
|
t1: {
|
||
|
|
chunk: [
|
||
|
|
{
|
||
|
|
event_id: '$3',
|
||
|
|
sender: '@me:hs',
|
||
|
|
type: 'm.room.member',
|
||
|
|
state_key: '@me:hs',
|
||
|
|
content: {},
|
||
|
|
},
|
||
|
|
{ event_id: '$4', sender: '@me:hs', type: 'm.sticker', content: { url: 'mxc://x' } },
|
||
|
|
],
|
||
|
|
end: 't2',
|
||
|
|
},
|
||
|
|
t2: { chunk: [], end: 't2' },
|
||
|
|
};
|
||
|
|
const mx = {
|
||
|
|
createMessagesRequest: async (_r: string, from: string | null) => pages[from ?? 'start'],
|
||
|
|
} as unknown as MatrixClient;
|
||
|
|
const progress: number[] = [];
|
||
|
|
const ids = await collectOwnEventIds(mx, '!r:hs', '@me:hs', (n) => progress.push(n));
|
||
|
|
assert.deepEqual(ids, ['$1', '$4']);
|
||
|
|
assert.deepEqual(progress, [1, 2, 2]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('redactPending is sequential, skips 404s, honours 429 and persists progress', async () => {
|
||
|
|
const calls: string[] = [];
|
||
|
|
let first429 = true;
|
||
|
|
const mx = {
|
||
|
|
redactEvent: async (_r: string, id: string) => {
|
||
|
|
calls.push(id);
|
||
|
|
if (id === '$gone') throw new MatrixError({ errcode: 'M_NOT_FOUND', error: 'nope' }, 404);
|
||
|
|
if (id === '$slow' && first429) {
|
||
|
|
first429 = false;
|
||
|
|
throw new MatrixError(
|
||
|
|
{ errcode: 'M_LIMIT_EXCEEDED', error: 'slow', retry_after_ms: 5 },
|
||
|
|
429,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return { event_id: '$red' };
|
||
|
|
},
|
||
|
|
} as unknown as MatrixClient;
|
||
|
|
const state: RedactJobState = {
|
||
|
|
roomId: '!r:hs',
|
||
|
|
phase: 'redacting',
|
||
|
|
pending: ['$a', '$gone', '$slow'],
|
||
|
|
found: 3,
|
||
|
|
redacted: 0,
|
||
|
|
skipped: 0,
|
||
|
|
leaveAfter: false,
|
||
|
|
};
|
||
|
|
const seen: number[] = [];
|
||
|
|
await redactPending(mx, state, (s) => seen.push(s.pending.length));
|
||
|
|
assert.deepEqual(calls, ['$a', '$gone', '$slow', '$slow']);
|
||
|
|
assert.equal(state.redacted, 2);
|
||
|
|
assert.equal(state.skipped, 1);
|
||
|
|
assert.deepEqual(seen, [2, 1, 0]);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('cancel aborts between events', async () => {
|
||
|
|
const ctrl = new AbortController();
|
||
|
|
const mx = {
|
||
|
|
redactEvent: async () => {
|
||
|
|
ctrl.abort();
|
||
|
|
return {};
|
||
|
|
},
|
||
|
|
} as unknown as MatrixClient;
|
||
|
|
const state: RedactJobState = {
|
||
|
|
roomId: '!r:hs',
|
||
|
|
phase: 'redacting',
|
||
|
|
pending: ['$a', '$b'],
|
||
|
|
found: 2,
|
||
|
|
redacted: 0,
|
||
|
|
skipped: 0,
|
||
|
|
leaveAfter: false,
|
||
|
|
};
|
||
|
|
await assert.rejects(() => redactPending(mx, state, () => undefined, ctrl.signal), /cancelled/);
|
||
|
|
assert.equal(state.redacted, 1);
|
||
|
|
assert.deepEqual(state.pending, ['$b']);
|
||
|
|
});
|