import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' // Mock fetch globally const mockFetch = vi.fn() vi.stubGlobal('fetch', mockFetch) // We need to reset modules between tests because nostr-auth.ts has module-level // state (currentToken initialized from localStorage at import time) function makeJwt(payload: Record): string { const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' })) const body = btoa(JSON.stringify(payload)) const sig = 'fakesignature' return `${header}.${body}.${sig}` } describe('nostr-auth token storage', () => { beforeEach(() => { vi.clearAllMocks() localStorage.clear() }) afterEach(() => { localStorage.clear() }) it('getToken returns null when no token set', async () => { vi.resetModules() const { getToken } = await import('../../lib/nostr-auth') expect(getToken()).toBeNull() }) it('setToken / getToken round-trips', async () => { vi.resetModules() const { setToken, getToken } = await import('../../lib/nostr-auth') const token = makeJwt({ sub: 'testpub', exp: Math.floor(Date.now() / 1000) + 3600 }) setToken(token) expect(getToken()).toBe(token) expect(localStorage.getItem('bf_token')).toBe(token) }) it('setToken(null) clears token from memory and localStorage', async () => { vi.resetModules() localStorage.setItem('bf_token', 'old-token') const { setToken, getToken } = await import('../../lib/nostr-auth') setToken(null) expect(getToken()).toBeNull() expect(localStorage.getItem('bf_token')).toBeNull() }) it('getToken restores from localStorage on module load', async () => { const token = makeJwt({ sub: 'pub123', exp: Math.floor(Date.now() / 1000) + 3600 }) localStorage.setItem('bf_token', token) vi.resetModules() const { getToken } = await import('../../lib/nostr-auth') expect(getToken()).toBe(token) }) }) describe('isTokenExpired', () => { beforeEach(() => { vi.clearAllMocks() localStorage.clear() }) afterEach(() => { localStorage.clear() }) it('returns true when no token is set', async () => { vi.resetModules() const { isTokenExpired } = await import('../../lib/nostr-auth') expect(isTokenExpired()).toBe(true) }) it('returns true for expired JWT', async () => { // exp in the past const expired = makeJwt({ sub: 'pub', exp: Math.floor(Date.now() / 1000) - 60 }) localStorage.setItem('bf_token', expired) vi.resetModules() const { isTokenExpired } = await import('../../lib/nostr-auth') expect(isTokenExpired()).toBe(true) }) it('returns false for valid (non-expired) JWT', async () => { // exp 1 hour in the future const valid = makeJwt({ sub: 'pub', exp: Math.floor(Date.now() / 1000) + 3600 }) localStorage.setItem('bf_token', valid) vi.resetModules() const { isTokenExpired } = await import('../../lib/nostr-auth') expect(isTokenExpired()).toBe(false) }) it('returns true for malformed token (not 3 parts)', async () => { localStorage.setItem('bf_token', 'not-a-jwt') vi.resetModules() const { setToken, isTokenExpired } = await import('../../lib/nostr-auth') setToken('not-a-jwt') expect(isTokenExpired()).toBe(true) }) it('returns true for token with no exp claim', async () => { const noExp = makeJwt({ sub: 'pub' }) localStorage.setItem('bf_token', noExp) vi.resetModules() const { isTokenExpired } = await import('../../lib/nostr-auth') expect(isTokenExpired()).toBe(true) }) }) describe('authFetch', () => { beforeEach(() => { vi.clearAllMocks() localStorage.clear() }) afterEach(() => { localStorage.clear() }) it('attaches Bearer token to request headers', async () => { const token = makeJwt({ sub: 'pub', exp: Math.floor(Date.now() / 1000) + 3600 }) localStorage.setItem('bf_token', token) vi.resetModules() const { authFetch } = await import('../../lib/nostr-auth') mockFetch.mockResolvedValueOnce({ status: 200, ok: true }) await authFetch('/api/test') expect(mockFetch).toHaveBeenCalledTimes(1) const [url, init] = mockFetch.mock.calls[0] expect(url).toBe('/api/test') const headers = init.headers as Headers expect(headers.get('Authorization')).toBe(`Bearer ${token}`) }) it('does not attach token when no token is set', async () => { vi.resetModules() const { authFetch } = await import('../../lib/nostr-auth') mockFetch.mockResolvedValueOnce({ status: 200, ok: true }) await authFetch('/api/public') const [, init] = mockFetch.mock.calls[0] const headers = init.headers as Headers expect(headers.get('Authorization')).toBeNull() }) it('does not attach expired token', async () => { const expired = makeJwt({ sub: 'pub', exp: Math.floor(Date.now() / 1000) - 60 }) localStorage.setItem('bf_token', expired) vi.resetModules() const { authFetch } = await import('../../lib/nostr-auth') mockFetch.mockResolvedValueOnce({ status: 200, ok: true }) await authFetch('/api/test') const [, init] = mockFetch.mock.calls[0] const headers = init.headers as Headers expect(headers.get('Authorization')).toBeNull() }) it('clears token on 401 response', async () => { const token = makeJwt({ sub: 'pub', exp: Math.floor(Date.now() / 1000) + 3600 }) localStorage.setItem('bf_token', token) vi.resetModules() const { authFetch, getToken } = await import('../../lib/nostr-auth') mockFetch.mockResolvedValueOnce({ status: 401, ok: false }) await authFetch('/api/protected') expect(getToken()).toBeNull() expect(localStorage.getItem('bf_token')).toBeNull() }) it('preserves token on non-401 error responses', async () => { const token = makeJwt({ sub: 'pub', exp: Math.floor(Date.now() / 1000) + 3600 }) localStorage.setItem('bf_token', token) vi.resetModules() const { authFetch, getToken } = await import('../../lib/nostr-auth') mockFetch.mockResolvedValueOnce({ status: 500, ok: false }) await authFetch('/api/broken') expect(getToken()).toBe(token) }) it('passes through custom request init options', async () => { const token = makeJwt({ sub: 'pub', exp: Math.floor(Date.now() / 1000) + 3600 }) localStorage.setItem('bf_token', token) vi.resetModules() const { authFetch } = await import('../../lib/nostr-auth') mockFetch.mockResolvedValueOnce({ status: 200, ok: true }) await authFetch('/api/data', { method: 'POST', body: JSON.stringify({ key: 'value' }), }) const [, init] = mockFetch.mock.calls[0] expect(init.method).toBe('POST') expect(init.body).toBe(JSON.stringify({ key: 'value' })) }) })