Files
accounted/lib/currency/__tests__/riksbanken.test.ts
T
2026-02-24 16:06:36 +01:00

289 lines
7.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import {
fetchExchangeRate,
fetchMultipleRates,
fetchRateRange,
fetchLatestRate,
convertToSEK,
formatCurrencyAmount,
} from '../riksbanken'
// Mock logger to suppress output
vi.mock('@/lib/logger', () => ({
createLogger: () => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}))
describe('fetchExchangeRate', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
it('returns rate 1 for SEK without fetching', async () => {
const fetchSpy = vi.spyOn(global, 'fetch')
const result = await fetchExchangeRate('SEK')
expect(result).toEqual({
currency: 'SEK',
rate: 1,
date: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/),
})
expect(fetchSpy).not.toHaveBeenCalled()
})
it('parses EUR rate from API response', async () => {
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
new Response(JSON.stringify([{ value: '11.42', date: '2025-01-15' }]), { status: 200 })
)
const result = await fetchExchangeRate('EUR', new Date('2025-01-15'))
expect(result).toEqual({
currency: 'EUR',
rate: 11.42,
date: '2025-01-15',
})
})
it('returns fallback rate on fetch error', async () => {
vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'))
const result = await fetchExchangeRate('EUR')
expect(result).not.toBeNull()
expect(result!.currency).toBe('EUR')
expect(result!.rate).toBeGreaterThan(0)
})
it('tries fallback URL when primary returns non-200', async () => {
vi.spyOn(global, 'fetch')
.mockResolvedValueOnce(new Response('Not Found', { status: 404 }))
.mockResolvedValueOnce(
new Response(JSON.stringify([
{ value: '10.80', date: '2025-01-13' },
{ value: '10.85', date: '2025-01-14' },
]), { status: 200 })
)
const result = await fetchExchangeRate('USD', new Date('2025-01-15'))
expect(result).toEqual({
currency: 'USD',
rate: 10.85,
date: '2025-01-14',
})
})
})
describe('fetchMultipleRates', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
it('returns Map with all requested currencies', async () => {
vi.spyOn(global, 'fetch')
.mockResolvedValueOnce(
new Response(JSON.stringify([{ value: '11.42', date: '2025-01-15' }]), { status: 200 })
)
.mockResolvedValueOnce(
new Response(JSON.stringify([{ value: '10.50', date: '2025-01-15' }]), { status: 200 })
)
const result = await fetchMultipleRates(['EUR', 'USD'])
expect(result.size).toBe(3) // EUR, USD, + always SEK
expect(result.get('SEK')!.rate).toBe(1)
expect(result.get('EUR')!.rate).toBe(11.42)
expect(result.get('USD')!.rate).toBe(10.50)
})
it('handles partial failure — returns fallback for failed currencies', async () => {
vi.spyOn(global, 'fetch')
.mockResolvedValueOnce(
new Response(JSON.stringify([{ value: '11.42', date: '2025-01-15' }]), { status: 200 })
)
.mockRejectedValueOnce(new Error('Network error'))
const result = await fetchMultipleRates(['EUR', 'GBP'])
expect(result.size).toBe(3)
expect(result.get('EUR')!.rate).toBe(11.42)
// GBP gets fallback rate (from the catch in fetchExchangeRate)
expect(result.get('GBP')).toBeDefined()
expect(result.get('GBP')!.rate).toBeGreaterThan(0)
})
it('returns only SEK when given empty array', async () => {
const result = await fetchMultipleRates([])
expect(result.size).toBe(1)
expect(result.get('SEK')!.rate).toBe(1)
})
it('handles SEK in the input array without duplicate fetch', async () => {
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
new Response(JSON.stringify([{ value: '11.42', date: '2025-01-15' }]), { status: 200 })
)
const result = await fetchMultipleRates(['SEK', 'EUR'])
expect(result.size).toBe(2)
expect(result.get('SEK')!.rate).toBe(1)
expect(result.get('EUR')!.rate).toBe(11.42)
})
})
describe('fetchRateRange', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
it('returns sorted array of rates', async () => {
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
new Response(JSON.stringify([
{ value: '11.40', date: '2025-01-13' },
{ value: '11.45', date: '2025-01-15' },
{ value: '11.42', date: '2025-01-14' },
]), { status: 200 })
)
const result = await fetchRateRange(
'EUR',
new Date('2025-01-13'),
new Date('2025-01-15')
)
expect(result).toHaveLength(3)
expect(result[0].date).toBe('2025-01-13')
expect(result[1].date).toBe('2025-01-14')
expect(result[2].date).toBe('2025-01-15')
})
it('returns [rate:1] for SEK', async () => {
const fetchSpy = vi.spyOn(global, 'fetch')
const result = await fetchRateRange(
'SEK',
new Date('2025-01-13'),
new Date('2025-01-15')
)
expect(result).toHaveLength(1)
expect(result[0].rate).toBe(1)
expect(fetchSpy).not.toHaveBeenCalled()
})
it('returns empty array on error', async () => {
vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'))
const result = await fetchRateRange(
'EUR',
new Date('2025-01-13'),
new Date('2025-01-15')
)
expect(result).toEqual([])
})
it('returns empty array on non-200 response', async () => {
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
new Response('Not Found', { status: 404 })
)
const result = await fetchRateRange(
'EUR',
new Date('2025-01-13'),
new Date('2025-01-15')
)
expect(result).toEqual([])
})
})
describe('fetchLatestRate', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
it('returns the last item from API response', async () => {
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
new Response(JSON.stringify([
{ value: '11.40', date: '2025-01-13' },
{ value: '11.42', date: '2025-01-14' },
{ value: '11.45', date: '2025-01-15' },
]), { status: 200 })
)
const result = await fetchLatestRate('EUR')
expect(result).toEqual({
currency: 'EUR',
rate: 11.45,
date: '2025-01-15',
})
})
it('returns rate 1 for SEK', async () => {
const fetchSpy = vi.spyOn(global, 'fetch')
const result = await fetchLatestRate('SEK')
expect(result).toEqual({
currency: 'SEK',
rate: 1,
date: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/),
})
expect(fetchSpy).not.toHaveBeenCalled()
})
it('returns fallback on error', async () => {
vi.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'))
const result = await fetchLatestRate('EUR')
expect(result).not.toBeNull()
expect(result!.currency).toBe('EUR')
expect(result!.rate).toBeGreaterThan(0)
})
it('returns null on empty API response', async () => {
vi.spyOn(global, 'fetch').mockResolvedValueOnce(
new Response(JSON.stringify([]), { status: 200 })
)
const result = await fetchLatestRate('EUR')
expect(result).toBeNull()
})
})
describe('convertToSEK', () => {
it('converts amount correctly', () => {
expect(convertToSEK(100, 11.42)).toBe(1142)
})
it('handles zero amount', () => {
expect(convertToSEK(0, 11.42)).toBe(0)
})
})
describe('formatCurrencyAmount', () => {
it('formats EUR with symbol prefix', () => {
const result = formatCurrencyAmount(1234.56, 'EUR')
// sv-SE uses non-breaking space as thousands separator
expect(result).toContain('€')
expect(result).toContain('1')
expect(result).toContain('234')
})
it('formats SEK with currency suffix', () => {
const result = formatCurrencyAmount(1234.56, 'SEK')
expect(result).toContain('SEK')
})
it('formats NOK with currency suffix', () => {
const result = formatCurrencyAmount(100, 'NOK')
expect(result).toContain('NOK')
})
})