Files
botfights/frontend/src/composables/__tests__/useNostr.test.ts
T

164 lines
5.6 KiB
TypeScript
Raw Normal View History

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()
2026-04-11 19:46:37 +01:00
sessionStorage.clear()
})
afterEach(() => {
localStorage.clear()
2026-04-11 19:46:37 +01:00
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'))
2026-04-11 19:46:37 +01:00
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()
2026-04-11 19:46:37 +01:00
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)
})
2026-04-11 19:46:37 +01:00
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()
})
2026-04-11 19:46:37 +01:00
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()
2026-04-11 19:46:37 +01:00
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
})
})