UNPKG

@nostr-dev-kit/ndk

Version:

NDK - Nostr Development Kit. Includes AI Guardrails to catch common mistakes during development.

140 lines (113 loc) 5.66 kB
import { beforeEach, describe, expect, it } from "vitest"; import type { NDKCacheAdapter } from "../cache"; import { NDK } from "../ndk"; import { NDKPrivateKeySigner } from "../signers/private-key"; import { giftUnwrap, giftWrap } from "./gift-wrapping.js"; import { NDKEvent } from "./index.js"; import { NDKKind } from "./kinds"; // Mock cache adapter to track cache operations class MockCacheAdapter implements Partial<NDKCacheAdapter> { public locking = false; private cache = new Map<NDKEvent["id"], NDKEvent>(); public getDecryptedEventCalls: string[] = []; public addDecryptedEventCalls: Array<{ wrapperId: string; rumorId: string }> = []; async getDecryptedEvent(wrapperId: NDKEvent["id"]): Promise<NDKEvent | null> { this.getDecryptedEventCalls.push(wrapperId); return this.cache.get(wrapperId) ?? null; } async addDecryptedEvent(wrapperId: NDKEvent["id"], decryptedEvent: NDKEvent): Promise<void> { this.addDecryptedEventCalls.push({ wrapperId, rumorId: decryptedEvent.id }); this.cache.set(wrapperId, decryptedEvent); } reset() { this.getDecryptedEventCalls = []; this.addDecryptedEventCalls = []; } clearCache() { this.cache.clear(); this.getDecryptedEventCalls = []; this.addDecryptedEventCalls = []; } } describe("NIP-17 Gift Wrapping Cache", () => { let ndk: NDK; let aliceSigner: NDKPrivateKeySigner; let bobSigner: NDKPrivateKeySigner; let mockCache: MockCacheAdapter; beforeEach(async () => { mockCache = new MockCacheAdapter(); ndk = new NDK({ cacheAdapter: mockCache as any }); aliceSigner = NDKPrivateKeySigner.generate(); bobSigner = NDKPrivateKeySigner.generate(); }); it("should cache decrypted gift-wrapped events using wrapper ID as key", async () => { const bob = await bobSigner.user(); // Alice creates a rumor const rumor = new NDKEvent(ndk); rumor.kind = NDKKind.PrivateDirectMessage; rumor.content = "Secret message"; rumor.created_at = Math.floor(Date.now() / 1000); rumor.tags = [["p", bob.pubkey]]; rumor.pubkey = (await aliceSigner.user()).pubkey; // Alice gift-wraps the rumor for Bob const wrapped = await giftWrap(rumor, bob, aliceSigner); wrapped.ndk = ndk; // First unwrap - should decrypt and cache mockCache.clearCache(); const unwrapped1 = await giftUnwrap(wrapped, undefined, bobSigner); expect(unwrapped1.content).toBe("Secret message"); expect(mockCache.getDecryptedEventCalls).toHaveLength(1); expect(mockCache.getDecryptedEventCalls[0]).toBe(wrapped.id); expect(mockCache.addDecryptedEventCalls).toHaveLength(1); expect(mockCache.addDecryptedEventCalls[0].wrapperId).toBe(wrapped.id); expect(mockCache.addDecryptedEventCalls[0].rumorId).toBe(unwrapped1.id); // Second unwrap - should hit cache, no decryption mockCache.reset(); const unwrapped2 = await giftUnwrap(wrapped, undefined, bobSigner); expect(unwrapped2.content).toBe("Secret message"); expect(unwrapped2.id).toBe(unwrapped1.id); expect(mockCache.getDecryptedEventCalls).toHaveLength(1); expect(mockCache.getDecryptedEventCalls[0]).toBe(wrapped.id); expect(mockCache.addDecryptedEventCalls).toHaveLength(0); // No cache write on hit // Third unwrap - should still hit cache mockCache.reset(); const unwrapped3 = await giftUnwrap(wrapped, undefined, bobSigner); expect(unwrapped3.content).toBe("Secret message"); expect(unwrapped3.id).toBe(unwrapped1.id); expect(mockCache.getDecryptedEventCalls).toHaveLength(1); expect(mockCache.addDecryptedEventCalls).toHaveLength(0); }); it("should use wrapper ID not rumor ID for cache key", async () => { const bob = await bobSigner.user(); const rumor = new NDKEvent(ndk); rumor.kind = NDKKind.PrivateDirectMessage; rumor.content = "Test message"; rumor.created_at = Math.floor(Date.now() / 1000); rumor.tags = [["p", bob.pubkey]]; rumor.pubkey = (await aliceSigner.user()).pubkey; const wrapped = await giftWrap(rumor, bob, aliceSigner); wrapped.ndk = ndk; const unwrapped = await giftUnwrap(wrapped, undefined, bobSigner); // Verify that the cache key is the wrapper ID, not the rumor ID expect(mockCache.addDecryptedEventCalls).toHaveLength(1); expect(mockCache.addDecryptedEventCalls[0].wrapperId).toBe(wrapped.id); expect(mockCache.addDecryptedEventCalls[0].rumorId).toBe(unwrapped.id); expect(mockCache.addDecryptedEventCalls[0].wrapperId).not.toBe(mockCache.addDecryptedEventCalls[0].rumorId); }); it("should work without cache adapter", async () => { // Create NDK without cache adapter const ndkNoCache = new NDK({}); const bob = await bobSigner.user(); const rumor = new NDKEvent(ndkNoCache); rumor.kind = NDKKind.PrivateDirectMessage; rumor.content = "Message without cache"; rumor.created_at = Math.floor(Date.now() / 1000); rumor.tags = [["p", bob.pubkey]]; rumor.pubkey = (await aliceSigner.user()).pubkey; const wrapped = await giftWrap(rumor, bob, aliceSigner); wrapped.ndk = ndkNoCache; // Should not throw, just skip caching const unwrapped = await giftUnwrap(wrapped, undefined, bobSigner); expect(unwrapped.content).toBe("Message without cache"); }); });