import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' // Mock nostr-tools before importing useNostr vi.mock('nostr-tools', () => ({ generateSecretKey: () => new Uint8Array(32).fill(1), getPublicKey: () => 'a'.repeat(64), })) vi.mock('nostr-tools/utils', () => ({ bytesToHex: (b: Uint8Array) => Array.from(b).map(x => x.toString(16).padStart(2, '0')).join(''), hexToBytes: (h: string) => new Uint8Array(h.match(/.{2}/g)!.map(x => parseInt(x, 16))), })) vi.mock('nostr-tools/nip19', () => ({ nsecEncode: (b: Uint8Array) => 'nsec1mock', })) // Mock the auth module const mockAuthFetch = vi.fn().mockResolvedValue({ json: () => Promise.resolve({ exists: false }) }) const mockSetToken = vi.fn() const mockGetToken = vi.fn().mockReturnValue(null) const mockIsTokenExpired = vi.fn().mockReturnValue(true) vi.mock('../../lib/nostr-auth', () => ({ buildNip98Token: vi.fn().mockResolvedValue('mock-nip98-token'), setToken: (...args: unknown[]) => mockSetToken(...args), getToken: () => mockGetToken(), isTokenExpired: () => mockIsTokenExpired(), authFetch: (...args: unknown[]) => mockAuthFetch(...args), })) describe('useNostr', () => { beforeEach(() => { vi.clearAllMocks() localStorage.clear() sessionStorage.clear() }) afterEach(() => { localStorage.clear() sessionStorage.clear() }) it('isLoggedIn is false when no pubkey or bot stored', async () => { vi.resetModules() const { useNostr } = await import('../useNostr') const { isLoggedIn, pubkey, bot } = useNostr() expect(isLoggedIn.value).toBe(false) expect(pubkey.value).toBeNull() expect(bot.value).toBeNull() }) it('logout clears all auth state from localStorage', async () => { localStorage.setItem('bf_pubkey', JSON.stringify('testpub')) localStorage.setItem('bf_bot', JSON.stringify({ id: 'b1', name: 'Bot' })) localStorage.setItem('bf_pic', JSON.stringify('https://example.com/pic.jpg')) sessionStorage.setItem('bf_nsec', 'secretkey') vi.resetModules() const { useNostr } = await import('../useNostr') const { logout } = useNostr() logout() expect(localStorage.getItem('bf_pubkey')).toBeNull() expect(localStorage.getItem('bf_bot')).toBeNull() expect(localStorage.getItem('bf_pic')).toBeNull() expect(sessionStorage.getItem('bf_nsec')).toBeNull() expect(mockSetToken).toHaveBeenCalledWith(null) }) it('hasExtension detects window.nostr', async () => { vi.resetModules() const { useNostr } = await import('../useNostr') const { hasExtension } = useNostr() // No NIP-07 extension in jsdom expect(hasExtension.value).toBe(false) }) it('hasStoredKey is false when no nsec in localStorage', async () => { vi.resetModules() const { useNostr } = await import('../useNostr') const { hasStoredKey } = useNostr() expect(hasStoredKey.value).toBe(false) }) it('hasStoredKey is true when nsec pre-set in sessionStorage', async () => { sessionStorage.setItem('bf_nsec', 'test-nsec') vi.resetModules() const { useNostr } = await import('../useNostr') const { hasStoredKey } = useNostr() expect(hasStoredKey.value).toBe(true) }) it('getStoredNsec returns null when no key stored', async () => { vi.resetModules() const { useNostr } = await import('../useNostr') const { getStoredNsec } = useNostr() expect(getStoredNsec()).toBeNull() }) it('persistKey saves session nsec to sessionStorage', async () => { vi.resetModules() const { useNostr } = await import('../useNostr') const { persistKey } = useNostr() // Without a session key, persist should be a no-op persistKey() expect(sessionStorage.getItem('bf_nsec')).toBeNull() }) it('pubkey and bot are readonly refs', async () => { vi.resetModules() const { useNostr } = await import('../useNostr') const result = useNostr() // pubkey and bot should be Readonly refs expect(result.pubkey).toBeDefined() expect(result.bot).toBeDefined() expect(result.isLoggedIn).toBeDefined() expect(result.isLoading).toBeDefined() }) it('waitForSigner resolves false when no extension', async () => { vi.resetModules() vi.useFakeTimers() const { useNostr } = await import('../useNostr') const { waitForSigner } = useNostr() const promise = waitForSigner(500) // Advance timers past the timeout vi.advanceTimersByTime(600) const result = await promise expect(result).toBe(false) vi.useRealTimers() }) it('BUG-F7: autoRestoreRan survives module re-import (HMR)', async () => { // Set up conditions that trigger auto-restore: pubkey stored, no bot, valid JWT localStorage.setItem('bf_pubkey', JSON.stringify('a'.repeat(64))) mockGetToken.mockReturnValue('valid-jwt') mockIsTokenExpired.mockReturnValue(false) mockAuthFetch.mockResolvedValue({ json: () => Promise.resolve({ exists: true, bot: { id: 'b1', name: 'Bot' } }) }) // Clear globalThis flag delete (globalThis as any).__bf_autoRestoreRan // First import triggers auto-restore vi.resetModules() await import('../useNostr') await new Promise(r => setTimeout(r, 0)) // flush microtasks expect(mockAuthFetch).toHaveBeenCalledTimes(1) // globalThis flag should be set expect((globalThis as any).__bf_autoRestoreRan).toBe(true) // Second import (simulating HMR) should NOT trigger auto-restore again vi.resetModules() mockAuthFetch.mockClear() await import('../useNostr') await new Promise(r => setTimeout(r, 0)) expect(mockAuthFetch).not.toHaveBeenCalled() // Clean up delete (globalThis as any).__bf_autoRestoreRan }) })