Files
accounted/extensions/general/arcim-migration/lib/__tests__/provider-client.test.ts
T
Jakob WennbergandClaude Sonnet 5 ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00

159 lines
6.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { createQueuedMockSupabase } from '@/tests/helpers'
const { mockBlGet } = vi.hoisted(() => ({ mockBlGet: vi.fn() }))
vi.mock('@/lib/supabase/server', () => ({
createServiceClient: vi.fn(),
createClient: vi.fn(),
}))
vi.mock('@/lib/providers/bjornlunden/oauth', () => ({
refreshBjornLundenToken: vi.fn().mockResolvedValue({
access_token: 'bl-app-token',
token_type: 'Bearer',
expires_in: 3600,
}),
}))
// Keep the real BjornLundenApiError (instanceof checks in provider-client)
// but replace the client so the /details probe is controllable per test.
vi.mock('@/lib/providers/bjornlunden/client', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/providers/bjornlunden/client')>()
return {
...actual,
// Must be a `function` (not an arrow) so `new BjornLundenClient()` works.
BjornLundenClient: vi.fn().mockImplementation(function mockClient() {
return { get: mockBlGet }
}),
}
})
import { createServiceClient } from '@/lib/supabase/server'
import { BjornLundenApiError } from '@/lib/providers/bjornlunden/client'
import {
submitProviderToken,
ProviderTokenInvalidError,
ConsentNotFoundError,
} from '../provider-client'
describe('submitProviderToken', () => {
let mock: ReturnType<typeof createQueuedMockSupabase>
beforeEach(() => {
vi.clearAllMocks()
mock = createQueuedMockSupabase()
vi.mocked(createServiceClient).mockReturnValue(mock.supabase as never)
})
const tablesTouched = () => vi.mocked(mock.supabase.from).mock.calls.map((c) => c[0])
// ── Consent ownership (IDOR guard) ────────────────────────────────
it('throws ConsentNotFoundError and writes NOTHING when the consent belongs to another company', async () => {
// Ownership check finds no row for (consentId, ownerCompanyId): the same
// result whether the consent does not exist or belongs to another tenant.
mock.enqueue({ data: [] })
await expect(
submitProviderToken('consent-other-tenant', 'bokio', 'tok', 'bokio-guid', 'company-A'),
).rejects.toBeInstanceOf(ConsentNotFoundError)
// Only the ownership read happened: no token upsert, no consent update.
expect(tablesTouched()).toEqual(['provider_consents'])
})
it('stores tokens when the consent belongs to the caller company', async () => {
mock.enqueue({ data: [{ id: 'consent-1' }] }) // ownership check
mock.enqueue({ data: null }) // token upsert
const result = await submitProviderToken('consent-1', 'bokio', 'tok', 'bokio-guid', 'company-A')
expect(result).toEqual({ success: true, consentId: 'consent-1' })
expect(tablesTouched()).toEqual(['provider_consents', 'provider_consent_tokens'])
})
// ── BL /details probe error classification ────────────────────────
it('does NOT map a 429 from the BL probe to ProviderTokenInvalidError', async () => {
mock.enqueue({ data: [{ id: 'consent-1' }] }) // ownership check
mockBlGet.mockRejectedValueOnce(new BjornLundenApiError('Björn Lunden API error: 429', 429))
const err: unknown = await submitProviderToken(
'consent-1',
'bjornlunden',
'client_credentials',
'user-key-guid',
'company-A',
).catch((e: unknown) => e)
expect(err).toBeInstanceOf(BjornLundenApiError)
expect(err).not.toBeInstanceOf(ProviderTokenInvalidError)
// The transient failure must not store the unverified key either.
expect(tablesTouched()).not.toContain('provider_consent_tokens')
})
it('does NOT map gateway-style 5xx (503) from the BL probe to ProviderTokenInvalidError', async () => {
mock.enqueue({ data: [{ id: 'consent-1' }] })
mockBlGet.mockRejectedValueOnce(new BjornLundenApiError('Björn Lunden API error: 503', 503))
const err: unknown = await submitProviderToken(
'consent-1',
'bjornlunden',
'client_credentials',
'user-key-guid',
'company-A',
).catch((e: unknown) => e)
expect(err).toBeInstanceOf(BjornLundenApiError)
expect(err).not.toBeInstanceOf(ProviderTokenInvalidError)
})
it('maps 500 from the BL probe to invalid credentials (sandbox-verified bad-key signal) and disables probe retries', async () => {
mock.enqueue({ data: [{ id: 'consent-1' }] })
mockBlGet.mockRejectedValueOnce(new BjornLundenApiError('Björn Lunden API error: 500', 500))
await expect(
submitProviderToken('consent-1', 'bjornlunden', 'client_credentials', 'user-key-guid', 'company-A'),
).rejects.toBeInstanceOf(ProviderTokenInvalidError)
// The probe must fail fast: a typo'd key answers 500, which the client's
// retry policy treats as retryable: retry is disabled per call.
expect(mockBlGet).toHaveBeenCalledTimes(1)
expect(mockBlGet).toHaveBeenCalledWith('bl-app-token', 'user-key-guid', '/details', {
retry: false,
})
})
it('maps 404 from the BL probe to invalid credentials', async () => {
mock.enqueue({ data: [{ id: 'consent-1' }] })
mockBlGet.mockRejectedValueOnce(new BjornLundenApiError('Björn Lunden API error: 404', 404))
await expect(
submitProviderToken('consent-1', 'bjornlunden', 'client_credentials', 'user-key-guid', 'company-A'),
).rejects.toBeInstanceOf(ProviderTokenInvalidError)
})
it('stores BL tokens (and labels the consent) when the probe succeeds', async () => {
mock.enqueue({ data: [{ id: 'consent-1' }] }) // ownership check
mock.enqueue({ data: null }) // consent company_name update
mock.enqueue({ data: null }) // token upsert
mockBlGet.mockResolvedValueOnce({ name: 'Testbolaget AB' })
const result = await submitProviderToken(
'consent-1',
'bjornlunden',
'client_credentials',
'user-key-guid',
'company-A',
)
expect(result).toEqual({ success: true, consentId: 'consent-1' })
expect(tablesTouched()).toEqual([
'provider_consents',
'provider_consents',
'provider_consent_tokens',
])
})
})