Declining a remote invite could show a raw 'MatrixError: [500] Internal server error' and appear to do nothing. Root causes were client-side: decline called mx.leave unconditionally, so re-clicking after a slow federated leave hit an already-left remote room that Synapse 500s on; the room was never forgotten so a 'leave' ghost lingered and re-invited a click; and the raw error string was shown. Add a shared declineInvite(mx, roomId) helper that only leaves when still in the room (invite/join/knock) and then forgets it (best-effort, first use of forget in the app). Route the InviteCard decline and both 'Decline All' paths through it, and replace the raw error with a friendly message (real error kept in console). Tests: declineInvite covered (6 cases); typecheck + full suite + build clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
64 lines
2.2 KiB
TypeScript
64 lines
2.2 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { declineInvite } from './matrix';
|
|
|
|
// declineInvite must only leave when we're still in the room (invite/join/knock)
|
|
// — re-leaving an already-left remote room is what Synapse 500s on — and must
|
|
// always forget afterwards (best-effort) so the ghost invite can't linger.
|
|
const makeMx = (
|
|
membership: string | undefined,
|
|
opts: { leaveRejects?: boolean; forgetRejects?: boolean } = {},
|
|
) => {
|
|
const calls: string[] = [];
|
|
const mx = {
|
|
getRoom: () => (membership === undefined ? null : { getMyMembership: () => membership }),
|
|
leave: async () => {
|
|
calls.push('leave');
|
|
if (opts.leaveRejects) throw new Error('leave failed');
|
|
return {};
|
|
},
|
|
forget: async () => {
|
|
calls.push('forget');
|
|
if (opts.forgetRejects) throw new Error('forget failed');
|
|
return {};
|
|
},
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
} as any;
|
|
return { mx, calls };
|
|
};
|
|
|
|
test('declineInvite: invite → leave then forget', async () => {
|
|
const { mx, calls } = makeMx('invite');
|
|
await declineInvite(mx, '!r:s');
|
|
assert.deepEqual(calls, ['leave', 'forget']);
|
|
});
|
|
|
|
test('declineInvite: already left → skips leave, still forgets (no double-leave 500)', async () => {
|
|
const { mx, calls } = makeMx('leave');
|
|
await declineInvite(mx, '!r:s');
|
|
assert.deepEqual(calls, ['forget']);
|
|
});
|
|
|
|
test('declineInvite: join → leave then forget', async () => {
|
|
const { mx, calls } = makeMx('join');
|
|
await declineInvite(mx, '!r:s');
|
|
assert.deepEqual(calls, ['leave', 'forget']);
|
|
});
|
|
|
|
test('declineInvite: no room object → only forget', async () => {
|
|
const { mx, calls } = makeMx(undefined);
|
|
await declineInvite(mx, '!r:s');
|
|
assert.deepEqual(calls, ['forget']);
|
|
});
|
|
|
|
test('declineInvite: forget failure is swallowed (best-effort)', async () => {
|
|
const { mx, calls } = makeMx('invite', { forgetRejects: true });
|
|
await declineInvite(mx, '!r:s'); // resolves despite forget throwing
|
|
assert.deepEqual(calls, ['leave', 'forget']);
|
|
});
|
|
|
|
test('declineInvite: genuine leave failure rejects', async () => {
|
|
const { mx } = makeMx('invite', { leaveRejects: true });
|
|
await assert.rejects(() => declineInvite(mx, '!r:s'), /leave failed/);
|
|
});
|