Files
accounted/lib/bookkeeping/__tests__/bas-catalog-client.test.ts
T
MattssonandClaude Fable 5 27383fd02b fix(inbox): surface full BAS catalog in BookDirectlyDialog account picker (#1543)
* fix(inbox): surface full BAS catalog in BookDirectlyDialog account picker

The account combobox in the inbox book-directly flow was fed only the
company's active chart, so typing a prefix like 65 showed just the two
activated 65xx accounts and a search for 6540 found nothing, which reads
as the account not existing. Pass the cached BAS catalogue (same pattern
as JournalEntryForm) so every standard account is searchable; picking a
not-yet-activated account flows through the existing
ActivateAccountsDialog rail at booking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bookkeeping): log BAS catalog fetch failures

Compliance swarm finding (SOC 2 CC7.2): loadBasCatalog swallowed fetch
errors silently, leaving catalog-load failures unobservable. Log inside
the client's catch, which is the only place the error actually surfaces:
callers' own .catch handlers are unreachable since the shared promise
already resolves to an empty list on failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(bookkeeping): unit-cover loadBasCatalog fetch, fallback, and retry

CodeRabbit finding on PR #1543: the catalog client had no focused
coverage. Tests assert the success path with promise caching, empty-list
fallback with logging on non-OK responses, missing data field handling,
and cache clearing after a failure so the next call refetches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 00:55:33 +02:00

75 lines
2.5 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
// loadBasCatalog caches its in-flight promise at module level, so every test
// re-imports a fresh module instance to start from an empty cache.
async function importFreshClient() {
vi.resetModules()
return import('@/lib/bookkeeping/bas-catalog-client')
}
const CATALOG = [
{ account_number: '6540', account_name: 'IT-tjänster', account_class: 6, account_group: '65', description: null },
]
describe('loadBasCatalog', () => {
let consoleErrorSpy: ReturnType<typeof vi.spyOn>
beforeEach(() => {
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
consoleErrorSpy.mockRestore()
vi.unstubAllGlobals()
})
it('returns the catalog on a successful fetch and caches it across calls', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ data: CATALOG }),
})
vi.stubGlobal('fetch', fetchMock)
const { loadBasCatalog } = await importFreshClient()
await expect(loadBasCatalog()).resolves.toEqual(CATALOG)
await expect(loadBasCatalog()).resolves.toEqual(CATALOG)
expect(fetchMock).toHaveBeenCalledTimes(1)
})
it('resolves to an empty list and logs on a non-OK response', async () => {
const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 500 })
vi.stubGlobal('fetch', fetchMock)
const { loadBasCatalog } = await importFreshClient()
await expect(loadBasCatalog()).resolves.toEqual([])
expect(consoleErrorSpy).toHaveBeenCalledWith(
'[bas-catalog] fetch failed:',
expect.any(Error),
)
})
it('resolves to an empty list when the body has no data field', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({}),
})
vi.stubGlobal('fetch', fetchMock)
const { loadBasCatalog } = await importFreshClient()
await expect(loadBasCatalog()).resolves.toEqual([])
})
it('clears the cache after a failure so the next call retries the fetch', async () => {
const fetchMock = vi
.fn()
.mockRejectedValueOnce(new Error('network down'))
.mockResolvedValueOnce({ ok: true, json: async () => ({ data: CATALOG }) })
vi.stubGlobal('fetch', fetchMock)
const { loadBasCatalog } = await importFreshClient()
await expect(loadBasCatalog()).resolves.toEqual([])
await expect(loadBasCatalog()).resolves.toEqual(CATALOG)
expect(fetchMock).toHaveBeenCalledTimes(2)
})
})