import { test } from 'node:test'; import assert from 'node:assert/strict'; // These modules touch localStorage at import/runtime. Provide a minimal mock // that records removed keys, then import dynamically (a static import would // hoist above the mock). const removed: string[] = []; const store = new Map(); // A Proxy so `Object.keys(localStorage)` (used by the prefix sweeps) sees the // stored keys, like the real Storage object. const api = { getItem: (k: string) => store.get(k) ?? null, setItem: (k: string, v: string) => { store.set(k, v); }, removeItem: (k: string) => { removed.push(k); store.delete(k); }, }; (globalThis as { localStorage?: unknown }).localStorage = new Proxy(api, { ownKeys: () => Array.from(store.keys()), getOwnPropertyDescriptor: (target, key) => typeof key === 'string' && store.has(key) ? { value: store.get(key), enumerable: true, configurable: true, writable: true } : Object.getOwnPropertyDescriptor(target, key), }); const { clearPlaintextCaches } = await import('./plaintextCaches'); test('clearPlaintextCaches removes every plaintext/PII localStorage key', () => { store.clear(); store.set('cinny_translation_cache_v1', '[]'); store.set('cinny_scheduled_messages_v1', '{}'); store.set('cinny_recent_searches_v1', '[]'); store.set('cinny_recent_forward_targets_v1', '[]'); store.set('cinny_recent_gifs_v1', '[]'); store.set('cinny_recent_stickers_v1', '[]'); store.set('lotus_outbox_v1', '{"userId":"@me:server","entries":[]}'); removed.length = 0; clearPlaintextCaches(); for (const key of [ 'cinny_translation_cache_v1', 'cinny_scheduled_messages_v1', 'cinny_recent_searches_v1', 'cinny_recent_forward_targets_v1', 'cinny_recent_gifs_v1', 'cinny_recent_stickers_v1', 'lotus_outbox_v1', // [Gitea #112] unsent message content ]) { assert.ok(removed.includes(key), `${key} cleared`); } assert.equal(store.size, 0, 'all keys gone from store'); }); test('clearPlaintextCaches clears the per-user nav-path store only when given a userId', () => { store.clear(); store.set('navToActivePath@me:server', '{}'); removed.length = 0; clearPlaintextCaches(); // no userId -> nav path untouched assert.ok(!removed.includes('navToActivePath@me:server'), 'nav path kept without userId'); clearPlaintextCaches('@me:server'); assert.ok(removed.includes('navToActivePath@me:server'), 'nav path cleared with userId'); }); test('clearPlaintextCaches wipes drafts (#41) and the status message (#204) but not session keys', () => { store.clear(); store.set('draft-msg-!room:server', '{"body":"unsent"}'); store.set('lotus-status-msg-@me:server', 'at the dentist'); store.set('lotus-status-expiry-@me:server', '123'); store.set('cinny_session', '{"accessToken":"x"}'); removed.length = 0; clearPlaintextCaches('@me:server'); assert.ok(removed.includes('draft-msg-!room:server'), 'draft cleared'); assert.ok(removed.includes('lotus-status-msg-@me:server'), 'status message cleared'); assert.ok(removed.includes('lotus-status-expiry-@me:server'), 'status expiry cleared'); assert.ok(!removed.includes('cinny_session'), 'session key not this module’s concern'); assert.ok(store.has('cinny_session')); });