Files
botfights/frontend/src/lib/__tests__/nostr-auth.test.ts
T
2026-04-11 19:46:37 +01:00

226 lines
6.6 KiB
TypeScript

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, unknown>): 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' }))
})
})