diff --git a/tests/nostr/sessionActor.test.js b/tests/nostr/sessionActor.test.js index e0853881..1aa6a725 100644 --- a/tests/nostr/sessionActor.test.js +++ b/tests/nostr/sessionActor.test.js @@ -49,26 +49,6 @@ test("encryptSessionPrivateKey + decryptSessionPrivateKey roundtrip", async () = assert.strictEqual(decrypted, privateKey); }); -test("persistSessionActor stores plain-text session actors", () => { - clearStoredSessionActor(); - - persistSessionActor({ - pubkey: "npub1test", - privateKey: "b".repeat(64), - createdAt: 123, - }); - - const raw = globalThis.localStorage.getItem(SESSION_ACTOR_STORAGE_KEY); - assert.ok(raw, "session actor should be persisted"); - - const parsed = JSON.parse(raw); - assert.deepStrictEqual(parsed, { - pubkey: "npub1test", - privateKey: "b".repeat(64), - createdAt: 123, - }); -}); - test("persistSessionActor stores encrypted payload metadata", async () => { clearStoredSessionActor(); @@ -106,11 +86,16 @@ test("persistSessionActor stores encrypted payload metadata", async () => { assert.strictEqual(parsed.privateKeyEncrypted, encrypted.ciphertext); }); -test("clearStoredSessionActor removes persisted payload", () => { +test("clearStoredSessionActor removes persisted payload", async () => { clearStoredSessionActor(); + const privateKey = "d".repeat(64); + const passphrase = "cleanup time"; + const encrypted = await encryptSessionPrivateKey(privateKey, passphrase); + persistSessionActor({ pubkey: "npub1cleanup", - privateKey: "d".repeat(64), + privateKeyEncrypted: encrypted.ciphertext, + encryption: encrypted, }); assert.ok(globalThis.localStorage.getItem(SESSION_ACTOR_STORAGE_KEY)); diff --git a/tests/state/profile-cache.test.mjs b/tests/state/profile-cache.test.mjs index 33e3519f..5fe37821 100644 --- a/tests/state/profile-cache.test.mjs +++ b/tests/state/profile-cache.test.mjs @@ -106,3 +106,53 @@ test("ProfileCache: setProfile normalizes and saves", () => { const stored = profileCache.getProfile(pubkey); assert.deepEqual(stored, entry.profile); }); + +test("ProfileCache: emits events on active profile change", () => { + let eventDetail = null; + const pubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + + const unsubscribe = profileCache.subscribe((event, detail) => { + if (event === "profileChanged") { + eventDetail = detail; + } + }); + + profileCache.setActiveProfile(pubkey); + assert.deepEqual(eventDetail, { pubkey }); + unsubscribe(); +}); + +test("ProfileCache: respects TTL expiration", () => { + const pubkey = "1111111111111111111111111111111111111111111111111111111111111111"; + const section = "watchHistory"; // 24h TTL + const ONE_DAY_MS = 24 * 60 * 60 * 1000; + + // Create expired data + const data = { history: [], savedAt: Date.now() - (ONE_DAY_MS + 1000) }; + const key = profileCache.getStorageKey(pubkey, section); + globalThis.localStorage.setItem(key, JSON.stringify(data)); + + // Ensure memory is empty + profileCache.clearMemoryCache(pubkey); + + const result = profileCache.getProfileData(pubkey, section); + assert.equal(result, null); +}); + +test("ProfileCache: setProfile sanitizes XSS in media URLs", () => { + const pubkey = "2222222222222222222222222222222222222222222222222222222222222222"; + const rawProfile = { + name: "Hacker", + picture: "javascript:alert(1)", + banner: "javascript:alert(2)", + }; + + const entry = profileCache.setProfile(pubkey, rawProfile); + + // sanitizeProfileMediaUrl returns null or fallback for invalid schemes + // "assets/svg/default-profile.svg" is default for picture + assert.equal(entry.profile.picture, "assets/svg/default-profile.svg"); + + // banner might be undefined if invalid + assert.equal(entry.profile.banner, undefined); +}); diff --git a/tests/state/profileCache.test.mjs b/tests/state/profileCache.test.mjs deleted file mode 100644 index eca0536f..00000000 --- a/tests/state/profileCache.test.mjs +++ /dev/null @@ -1,170 +0,0 @@ -import { test, describe, beforeEach } from "node:test"; -import assert from "node:assert"; -import { profileCache } from "../../js/state/profileCache.js"; -import { NOTE_TYPES } from "../../js/nostrEventSchemas.js"; - -describe("ProfileCache", () => { - const TEST_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000001"; - const ANOTHER_PUBKEY = "0000000000000000000000000000000000000000000000000000000000000002"; - - beforeEach(() => { - // Reset internal state - profileCache.activePubkey = null; - profileCache.memoryCache.clear(); - profileCache.listeners.clear(); - // Clear mock localStorage - if (globalThis.localStorage) { - globalThis.localStorage.clear(); - } - }); - - test("setActiveProfile normalizes and sets active pubkey", () => { - profileCache.setActiveProfile(" " + TEST_PUBKEY.toUpperCase() + " "); - assert.strictEqual(profileCache.getActiveProfile(), TEST_PUBKEY); - }); - - test("setActiveProfile clears memory cache for old profile", () => { - profileCache.setActiveProfile(TEST_PUBKEY); - profileCache.setMemoryDataForPubkey(TEST_PUBKEY, "test_section", { foo: "bar" }); - assert.ok(profileCache.getProfileData(TEST_PUBKEY, "test_section")); - - // Switch profile - profileCache.setActiveProfile(ANOTHER_PUBKEY); - - // Check old data is gone from memory - // Note: profileCache.getProfileData falls back to localStorage, so we should ensure localStorage is empty - // or specifically check memoryCache. - const memKey = `${TEST_PUBKEY}:test_section`; - assert.strictEqual(profileCache.memoryCache.has(memKey), false); - }); - - test("resolveAddressKey generates correct keys based on policy", () => { - profileCache.setActiveProfile(TEST_PUBKEY); - - // Watch History: kind:pubkey:d (dTag default) - // NOTE_TYPES.WATCH_HISTORY is usually 30078 or similar. - // But CACHE_POLICIES keys are the noteType string from schemas. - // Let's rely on what the code actually does. - - const key = profileCache.resolveAddressKey("watchHistory", { pubkey: TEST_PUBKEY }); - // Expect: bitvid:profile:{pubkey}:{noteType}:{dTag}:v1 - // The code maps "watchHistory" to NOTE_TYPES.WATCH_HISTORY via SECTION_TO_NOTE_TYPE. - const expectedPrefix = `bitvid:profile:${TEST_PUBKEY}:${NOTE_TYPES.WATCH_HISTORY}`; - assert.ok(key.startsWith(expectedPrefix)); - assert.ok(key.includes(":v1")); - }); - - test("setProfile normalizes and persists profile data", () => { - profileCache.setActiveProfile(TEST_PUBKEY); - - const rawProfile = { - name: " Test User ", - picture: "https://example.com/pic.jpg", - about: " About me ", - website: " https://example.com ", - lud16: "user@ln.address", - }; - - profileCache.setProfile(TEST_PUBKEY, rawProfile); - - const stored = profileCache.getProfile(TEST_PUBKEY); - assert.ok(stored); - assert.strictEqual(stored.name, "Test User"); // Trimmed - assert.strictEqual(stored.picture, "https://example.com/pic.jpg"); - assert.strictEqual(stored.about, "About me"); // Trimmed - assert.strictEqual(stored.website, "https://example.com"); // Trimmed - assert.strictEqual(stored.lud16, "user@ln.address"); - - // Check persistence - // "profile" section maps to NOTE_TYPES.PROFILE_METADATA (kind 0) - // Policy addressing is kind:pubkey - const storageKey = profileCache.getStorageKey(TEST_PUBKEY, "profile"); - const storedJson = localStorage.getItem(storageKey); - assert.ok(storedJson, "Should be saved to localStorage"); - const parsed = JSON.parse(storedJson); - assert.deepStrictEqual(parsed.profile, stored); - }); - - test("getProfileData checks TTL", (t) => { - // Mock Date.now - const originalDateNow = Date.now; - let mockNow = 1000000; - Date.now = () => mockNow; - - try { - // NOTE_TYPES.VIDEO_POST has 10 min TTL (600,000 ms) - // We need to use a section that maps to a policy with TTL. - // "profile" has Infinity TTL. - // Let's look at CACHE_POLICIES. - // VIDEO_POST is indexedDB, so ProfileCache might not handle it via localStorage path? - // ProfileCache.getStorageTier checks policy. - // CACHE_POLICIES[NOTE_TYPES.VIDEO_POST].storage is INDEXED_DB. - - // We need a LOCAL_STORAGE policy with finite TTL. - // WATCH_HISTORY is LOCAL_STORAGE and 24h TTL. - - const section = "watchHistory"; - profileCache.setActiveProfile(TEST_PUBKEY); - - const data = { - items: [], - timestamp: mockNow // Saved at 1000000 - }; - - profileCache.set(section, data); - - // Verify immediate retrieval - const retrieved = profileCache.get(section); - assert.deepStrictEqual(retrieved, data); - - // Fast forward 25 hours (25 * 60 * 60 * 1000 = 90,000,000) - mockNow += 90000000; - - // Clear memory cache to force load from storage - profileCache.memoryCache.clear(); - - const expired = profileCache.get(section); - assert.strictEqual(expired, null, "Should return null for expired data"); - - } finally { - Date.now = originalDateNow; - } - }); - - test("setProfile sanitizes XSS in media URLs", () => { - const maliciousProfile = { - name: "Hacker", - picture: "javascript:alert(1)", - banner: "data:image/svg+xml;base64,PHN2ZyBvbG9hZD0iYWxlcnQoMSkiPjwvc3ZnPg==", // basic svg xss vector - }; - - profileCache.setProfile(TEST_PUBKEY, maliciousProfile); - const stored = profileCache.getProfile(TEST_PUBKEY); - - // sanitizeProfileMediaUrl returns empty string or null for invalid/dangerous schemes - // The implementation uses `sanitizeProfileMediaUrl` from `../utils/profileMedia.js`. - // If it detects bad schemes, it might clear it. - // Assuming "javascript:" is stripped. - assert.notStrictEqual(stored.picture, "javascript:alert(1)"); - // If it returns null or empty string, that's good. - - // Note: We are relying on `sanitizeProfileMediaUrl` behavior here. - // If that function is robust, these assertions pass. - }); - - test("emit fires events on update", (t, done) => { - profileCache.setActiveProfile(TEST_PUBKEY); - - const unsubscribe = profileCache.subscribe((event, detail) => { - if (event === "update") { - assert.strictEqual(detail.pubkey, TEST_PUBKEY); - assert.strictEqual(detail.section, "test_section"); - assert.deepStrictEqual(detail.data, { foo: "bar" }); - unsubscribe(); - done(); - } - }); - - profileCache.set("test_section", { foo: "bar" }); - }); -});