CI / Build & Quality Checks (pull_request) Successful in 2m43s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 12s
CI / Playwright smoke (e2e) (pull_request) Successful in 16m49s
The service worker posts inline notification replies in the clear (it cannot encrypt). The Reply action was only withheld for rooms that were encrypted when the notification was shown, so a reply typed hours later into a room that has since turned on encryption went out unencrypted. It also posted with the newest session the worker knew, not the notification's account. The worker now GETs the room's m.room.encryption state first and only sends on a definite 404; notifications carry the user id and the worker uses that account's session (or refuses). "Reply not sent" says why. Fixes #299 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
89 lines
3.5 KiB
TypeScript
89 lines
3.5 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
buildReplyContent,
|
|
pickReplySession,
|
|
replyFailureText,
|
|
replySendUrl,
|
|
sendNotificationReply,
|
|
} from './swReply';
|
|
|
|
test('thread replies carry the m.thread relation, room replies do not', () => {
|
|
assert.deepEqual(buildReplyContent('hi'), { msgtype: 'm.text', body: 'hi', 'm.mentions': {} });
|
|
assert.deepEqual(buildReplyContent('hi', '$root')['m.relates_to'], {
|
|
rel_type: 'm.thread',
|
|
event_id: '$root',
|
|
is_falling_back: true,
|
|
'm.in_reply_to': { event_id: '$root' },
|
|
});
|
|
});
|
|
|
|
test('send URL encodes the room id against the session base URL', () => {
|
|
assert.equal(
|
|
replySendUrl('https://matrix.example.org/', '!r:example.org', 'tx1'),
|
|
'https://matrix.example.org/_matrix/client/v3/rooms/!r%3Aexample.org/send/m.room.message/tx1',
|
|
);
|
|
});
|
|
|
|
test('sendNotificationReply checks encryption, then PUTs with the bearer token (#203, #299)', async () => {
|
|
const calls: { url: string; init: RequestInit }[] = [];
|
|
const fetchFor = (encStatus: number, sendOk = true) =>
|
|
(async (url: string, init: RequestInit = {}) => {
|
|
calls.push({ url, init });
|
|
if (url.includes('/state/m.room.encryption/')) {
|
|
return { ok: encStatus === 200, status: encStatus } as Response;
|
|
}
|
|
return { ok: sendOk, status: sendOk ? 200 : 500 } as Response;
|
|
}) as unknown as typeof fetch;
|
|
const session = { accessToken: 'tok', baseUrl: 'https://hs' };
|
|
|
|
assert.equal(await sendNotificationReply(session, '!r:hs', '$t', 'yo', fetchFor(404)), 'sent');
|
|
assert.match(calls[0].url, /\/rooms\/!r%3Ahs\/state\/m\.room\.encryption\/$/);
|
|
const put = calls[1];
|
|
assert.equal(put.init.method, 'PUT');
|
|
assert.equal((put.init.headers as Record<string, string>).Authorization, 'Bearer tok');
|
|
assert.match(
|
|
put.url,
|
|
/^https:\/\/hs\/_matrix\/client\/v3\/rooms\/!r%3Ahs\/send\/m\.room\.message\/swreply-/,
|
|
);
|
|
assert.equal(JSON.parse(put.init.body as string)['m.relates_to'].event_id, '$t');
|
|
|
|
// Encryption turned on since the notification was shown: nothing is sent.
|
|
calls.length = 0;
|
|
assert.equal(
|
|
await sendNotificationReply(session, '!r:hs', undefined, 'yo', fetchFor(200)),
|
|
'encrypted',
|
|
);
|
|
assert.equal(calls.length, 1);
|
|
// Can't tell (server error): fail safe, nothing is sent.
|
|
calls.length = 0;
|
|
assert.equal(
|
|
await sendNotificationReply(session, '!r:hs', undefined, 'yo', fetchFor(500)),
|
|
'failed',
|
|
);
|
|
assert.equal(calls.length, 1);
|
|
assert.equal(
|
|
await sendNotificationReply(session, '!r:hs', undefined, 'yo', fetchFor(404, false)),
|
|
'failed',
|
|
);
|
|
assert.equal(
|
|
await sendNotificationReply(undefined, '!r:hs', undefined, 'yo', fetchFor(404)),
|
|
'no-session',
|
|
);
|
|
});
|
|
|
|
test('pickReplySession: the notification account only (#299)', () => {
|
|
const a = { accessToken: 'a', baseUrl: 'https://hs', userId: '@a:hs' };
|
|
const b = { accessToken: 'b', baseUrl: 'https://hs', userId: '@b:hs' };
|
|
assert.equal(pickReplySession([a, b], '@a:hs'), a);
|
|
assert.equal(pickReplySession([b], '@a:hs'), undefined, 'logged out of @a here');
|
|
assert.equal(pickReplySession([a], undefined), a, 'old notification, one session');
|
|
assert.equal(pickReplySession([a, b], undefined), undefined, 'old notification, ambiguous');
|
|
});
|
|
|
|
test('replyFailureText says why', () => {
|
|
assert.match(replyFailureText('encrypted', 'hi'), /now encrypted/);
|
|
assert.match(replyFailureText('no-session', 'hi'), /no longer signed in/);
|
|
assert.match(replyFailureText('failed', 'hi'), /retry/);
|
|
});
|