import { test } from 'node:test'; import assert from 'node:assert/strict'; import { MatrixClient, MatrixEvent } from 'matrix-js-sdk'; import { buildForwardContent } from './forwardContent'; // Pure content builder buildForwardContent: refuses undecryptable events, forwards // the latest edit (`m.new_content`), and strips reply fallbacks + `m.relates_to`. // MatrixClient / MatrixEvent are mocked minimally. getEditedEvent reads edits off // `timelineSet.relations.getChildEventsForEvent(...).getRelations()`, so the base // client returns no child edits and the edit test injects one. const SENDER = '@me:example.org'; type EventOptions = { content?: Record; type?: string; id?: string; roomId?: string; decryptionFailure?: boolean; ts?: number; }; const makeEvent = (options: EventOptions = {}): MatrixEvent => { const { content = {}, type = 'm.room.message', id = '$evt:example.org', roomId = '!room:example.org', decryptionFailure = false, ts = 0, } = options; return { getContent: () => content, getType: () => type, getId: () => id, getRoomId: () => roomId, getSender: () => SENDER, getTs: () => ts, isDecryptionFailure: () => decryptionFailure, } as unknown as MatrixEvent; }; // Base client: the timeline reports no `m.replace` edits, so the original content // is forwarded unchanged. const makeClient = (): MatrixClient => ({ getRoom: () => ({ getUnfilteredTimelineSet: () => ({ relations: { getChildEventsForEvent: () => null, }, }), }), }) as unknown as MatrixClient; test('plain text forwards the body and strips m.relates_to', () => { const mx = makeClient(); const mEvent = makeEvent({ content: { msgtype: 'm.text', body: 'hello world', 'm.relates_to': { rel_type: 'm.thread', event_id: '$root:example.org' }, }, }); const content = buildForwardContent(mx, mEvent); assert.ok(content); assert.equal(content.body, 'hello world'); assert.equal(content.msgtype, 'm.text'); assert.equal(content['m.relates_to'], undefined); }); test('reply quote is stripped from body and formatted_body', () => { const mx = makeClient(); const mEvent = makeEvent({ content: { msgtype: 'm.text', body: '> <@alice:example.org> original\n\nmy reply', format: 'org.matrix.custom.html', formatted_body: '
original
my reply', 'm.relates_to': { 'm.in_reply_to': { event_id: '$root:example.org' } }, }, }); const content = buildForwardContent(mx, mEvent); assert.ok(content); assert.equal(content.body, 'my reply'); assert.equal(content.formatted_body, 'my reply'); assert.equal(content['m.relates_to'], undefined); }); test('decryption failure returns undefined', () => { const mx = makeClient(); const mEvent = makeEvent({ content: { msgtype: 'm.bad.encrypted' }, decryptionFailure: true, }); assert.equal(buildForwardContent(mx, mEvent), undefined); }); test('edited message forwards m.new_content', () => { const mEvent = makeEvent({ content: { msgtype: 'm.text', body: 'original body', 'm.relates_to': { rel_type: 'm.thread', event_id: '$root:example.org' }, }, }); // The latest `m.replace` edit carries the new content under `m.new_content`. const editEvent = makeEvent({ content: { 'm.new_content': { msgtype: 'm.text', body: 'edited body' } }, ts: 100, }); const mx = { getRoom: () => ({ getUnfilteredTimelineSet: () => ({ relations: { getChildEventsForEvent: () => ({ getRelations: () => [editEvent], }), }, }), }), } as unknown as MatrixClient; const content = buildForwardContent(mx, mEvent); assert.ok(content); assert.equal(content.body, 'edited body'); assert.equal(content.msgtype, 'm.text'); assert.equal(content['m.new_content'], undefined); assert.equal(content['m.relates_to'], undefined); });