For encrypted rooms, bookmarks persist only {roomId, eventId, savedAt}
and reminders only their non-text fields; the preview, room name and
sender resolve locally at render/fire time from the timeline (with a
"Message unavailable" fallback). A one-time, loop-guarded cleanup strips
text from existing entries in currently-encrypted rooms. Unencrypted
rooms are unchanged. Unit-tested.
Fixes #10
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
Bookmark,
|
|
cleanupEncryptedBookmarks,
|
|
hasBookmarkText,
|
|
stripBookmarkText,
|
|
toStorableBookmark,
|
|
} from './useBookmarks';
|
|
import { cleanupEncryptedReminders, Reminder, toStorableReminder } from './useReminders';
|
|
|
|
// E2EE policy for bookmarks/reminders (Gitea #10): account data is stored
|
|
// unencrypted on the homeserver, so entries for encrypted rooms must carry no
|
|
// message text or names. `isEncryptedRoom` is injected to keep these pure.
|
|
const isEncrypted = (roomId: string) => roomId === '!enc';
|
|
|
|
const full = (roomId: string): Bookmark => ({
|
|
roomId,
|
|
eventId: `$ev-${roomId}`,
|
|
savedAt: 100,
|
|
previewText: 'secret text',
|
|
roomName: 'Room',
|
|
senderName: 'Alice',
|
|
});
|
|
|
|
test('stripBookmarkText keeps only roomId/eventId/savedAt', () => {
|
|
const stripped = stripBookmarkText(full('!enc'));
|
|
assert.deepEqual(stripped, { roomId: '!enc', eventId: '$ev-!enc', savedAt: 100, roomName: '' });
|
|
assert.equal(hasBookmarkText(stripped), false);
|
|
assert.equal(hasBookmarkText(full('!enc')), true);
|
|
});
|
|
|
|
test('toStorableBookmark strips text for encrypted rooms only', () => {
|
|
const plain = full('!plain');
|
|
assert.equal(toStorableBookmark(plain, isEncrypted), plain);
|
|
const enc = toStorableBookmark(full('!enc'), isEncrypted);
|
|
assert.equal(enc.previewText, undefined);
|
|
assert.equal(enc.senderName, undefined);
|
|
assert.equal(enc.roomName, '');
|
|
});
|
|
|
|
test('cleanupEncryptedBookmarks returns undefined when nothing to strip', () => {
|
|
const already = stripBookmarkText(full('!enc'));
|
|
assert.equal(cleanupEncryptedBookmarks([full('!plain'), already], isEncrypted), undefined);
|
|
assert.equal(cleanupEncryptedBookmarks([], isEncrypted), undefined);
|
|
});
|
|
|
|
test('cleanupEncryptedBookmarks strips legacy encrypted entries and keeps the rest', () => {
|
|
const plain = full('!plain');
|
|
const out = cleanupEncryptedBookmarks([plain, full('!enc')], isEncrypted);
|
|
assert.ok(out);
|
|
assert.equal(out[0], plain);
|
|
assert.deepEqual(out[1], stripBookmarkText(full('!enc')));
|
|
// Running it again on the result is a no-op (no write loop).
|
|
assert.equal(cleanupEncryptedBookmarks(out, isEncrypted), undefined);
|
|
});
|
|
|
|
const reminder = (roomId: string): Reminder => ({
|
|
roomId,
|
|
eventId: `$ev-${roomId}`,
|
|
timestamp: 200,
|
|
message: 'secret text',
|
|
});
|
|
|
|
test('toStorableReminder drops message for encrypted rooms only', () => {
|
|
const plain = reminder('!plain');
|
|
assert.equal(toStorableReminder(plain, isEncrypted), plain);
|
|
assert.deepEqual(toStorableReminder(reminder('!enc'), isEncrypted), {
|
|
roomId: '!enc',
|
|
eventId: '$ev-!enc',
|
|
timestamp: 200,
|
|
});
|
|
});
|
|
|
|
test('cleanupEncryptedReminders strips legacy entries once', () => {
|
|
const out = cleanupEncryptedReminders([reminder('!plain'), reminder('!enc')], isEncrypted);
|
|
assert.ok(out);
|
|
assert.equal(out[0].message, 'secret text');
|
|
assert.equal(out[1].message, undefined);
|
|
assert.equal(cleanupEncryptedReminders(out, isEncrypted), undefined);
|
|
});
|