226 lines
6.6 KiB
TypeScript
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' }))
|
|
})
|
|
})
|