Files
cinny/src/app/features/message-search/useLocalMessageSearch.test.ts
T
jaredandClaude Opus 5 c8e49d3855 fix(security): persistent search index forgets redacted and left-room text
Nothing ever removed an indexed row: redacted messages stayed searchable
with full plaintext and rendered as normal results. Now: a client-level
RoomEvent.Redaction listener deletes the row, leave/ban clears the room
(clearRoom finally has a caller), m.replace edits upsert the original
row instead of indexing the "* fallback" separately, and cached rows
whose local event is redacted render through the existing
redacted_because placeholder. Unit-tested.

Fixes #14

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-09-12 19:46:05 -04:00

57 lines
2.3 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { SearchCacheRow } from '../../utils/searchCache';
// useLocalMessageSearch.ts imports searchCacheEnabledAtom, which touches
// localStorage at module-load time (atomWithLocalStorage reads the initial
// value eagerly). Stub it before a dynamic import — a static import would
// hoist above the stub. Same pattern as state/plaintextCaches.test.ts.
(globalThis as { localStorage?: unknown }).localStorage = {
getItem: () => null,
setItem: () => {},
removeItem: () => {},
};
const { rowToResultItem } = await import('./useLocalMessageSearch');
const row = (overrides: Partial<SearchCacheRow> = {}): SearchCacheRow => ({
roomId: '!r1',
eventId: '$1',
ts: 100,
sender: '@a',
body: 'hello world',
...overrides,
});
// Gitea #14 — cached rows for a locally-known-redacted event must carry a
// `redacted_because` marker so SearchResultGroup's guard renders the
// "message deleted" placeholder instead of the stale plaintext.
test('rowToResultItem: plain row has no redacted_because marker', () => {
const item = rowToResultItem(row());
// eslint-disable-next-line @typescript-eslint/no-explicit-any
assert.equal((item.event as any).unsigned?.redacted_because, undefined);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
assert.equal((item.event as any).content.body, 'hello world');
});
test('rowToResultItem: redacted=true sets the redacted_because marker', () => {
const item = rowToResultItem(row(), true);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
assert.ok((item.event as any).unsigned?.redacted_because);
});
test('rowToResultItem: falls back to pollText when body is empty', () => {
const item = rowToResultItem(row({ body: '', pollText: 'question answer' }));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
assert.equal((item.event as any).content.body, 'question answer');
});
test('rowToResultItem: carries formattedBody as HTML when present', () => {
const item = rowToResultItem(row({ formattedBody: '<b>hi</b>' }));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const content = (item.event as any).content;
assert.equal(content.format, 'org.matrix.custom.html');
assert.equal(content.formatted_body, '<b>hi</b>');
});