* fix(connect): PR #1758 CodeRabbit follow-up: harden connector status, i18n the connector-mode strings, align docs - getConnectorConfig() rebuilds baseUrl as origin + path: userinfo, query and fragment are stripped (warn-logged without the raw value) so nothing secret-shaped pasted into GNUBOK_CONNECT_URL survives into the /api/connector/status echo or the derived proxy URLs (CWE-200) - /api/connector/status responds Cache-Control: no-store on both branches (key prefix + wiring layout out of shared browser caches, CWE-525) - CWE-319 thread verified as no-change: both connector-mode helpers derive from getConnectorConfig(), which fails closed on non-https - SkatteverketConnectPanel tooltips and BankSyncNowButton gate/upsell strings moved to messages/sv.json + messages/en.json keys - DECISIONS.md: MD037 fix on line 1146 (backtick the glob), line 1147 reworded to grants-written-wiring-pending, decision lines appended (incl. declining the UpgradeNote children-append suggestion) - docs/SOVEREIGN.md availability wording aligned with SELF-HOSTING.md: infra merged, keys issued manually on request, client wiring pending Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UKZUp1nePr8sVDoLkMSxbS * docs(connect): skeptic follow-up: bank client wiring is merged (#2094), SKV pending, no keys issued until it lands Skeptic refutation on PR #2098: SOVEREIGN.md claimed the services 'do not carry traffic' while this branch already contains #2094 (EB client proxy routing), and 'issued manually on request' contradicted the standing no-key-before-full-PR6b rule while skatteverketConnectorMode() has no client consumer yet. SOVEREIGN.md, SELF-HOSTING.md and DECISIONS.md line 1147 now all say: bank client wiring merged and carries traffic with a key, Skatteverket client wiring ships in a following release, keys are not issued until it lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UKZUp1nePr8sVDoLkMSxbS --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
71 lines
3.8 KiB
TypeScript
71 lines
3.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { createMockRequest, parseJsonResponse } from '@/tests/helpers'
|
|
|
|
let selfHosted = true
|
|
vi.mock('@/lib/env/public-flags', () => ({ isSelfHosted: () => selfHosted }))
|
|
vi.mock('@/lib/api/with-route-context', () => ({
|
|
withRouteContext: (_op: string, handler: (req: unknown, ctx: unknown) => unknown) => (req: unknown) =>
|
|
handler(req, { supabase: {}, companyId: 'company-1', user: { id: 'u1' } }),
|
|
}))
|
|
const held = vi.fn()
|
|
vi.mock('@/lib/entitlements/has-capability', () => ({ getCompanyIdsWithCapability: (...a: unknown[]) => held(...a) }))
|
|
|
|
import { GET } from '../route'
|
|
|
|
const ENV = ['GNUBOK_CONNECTOR_KEY', 'GNUBOK_CONNECT_URL', 'ENABLE_BANKING_APP_ID', 'SKATTEVERKET_APIGW_CLIENT_ID'] as const
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
selfHosted = true
|
|
for (const k of ENV) vi.stubEnv(k, '')
|
|
held.mockResolvedValue(new Set())
|
|
})
|
|
afterEach(() => vi.unstubAllEnvs())
|
|
|
|
describe('GET /api/connector/status', () => {
|
|
it('reports self_hosted:false on hosted', async () => {
|
|
selfHosted = false
|
|
const { body } = await parseJsonResponse<{ data: { self_hosted: boolean } }>(await GET(createMockRequest('/api/connector/status'), { params: Promise.resolve({}) }))
|
|
expect(body.data.self_hosted).toBe(false)
|
|
})
|
|
|
|
it('reports unconfigured when no key is set', async () => {
|
|
const { body } = await parseJsonResponse<{ data: { configured: boolean; upstreams: Record<string, string> } }>(await GET(createMockRequest('/x'), { params: Promise.resolve({}) }))
|
|
expect(body.data.configured).toBe(false)
|
|
expect(body.data.upstreams).toEqual({ bank: 'unconfigured', skatteverket: 'unconfigured' })
|
|
})
|
|
|
|
it('reports connector mode per upstream and the key prefix (never the key)', async () => {
|
|
vi.stubEnv('GNUBOK_CONNECTOR_KEY', 'gnubok_ck_secretsecret')
|
|
held.mockImplementation((_s: unknown, _ids: unknown, cap: string) => Promise.resolve(cap === 'bank_sync' ? new Set(['company-1']) : new Set()))
|
|
const { body } = await parseJsonResponse<{ data: { configured: boolean; key_prefix: string; upstreams: Record<string, string>; granted_capabilities: string[] } }>(await GET(createMockRequest('/x'), { params: Promise.resolve({}) }))
|
|
expect(body.data.configured).toBe(true)
|
|
expect(body.data.key_prefix).toBe('gnubok_ck_sec')
|
|
expect(body.data.key_prefix).not.toContain('secretsecret')
|
|
expect(body.data.upstreams).toEqual({ bank: 'connector', skatteverket: 'connector' })
|
|
expect(body.data.granted_capabilities).toEqual(['bank_sync'])
|
|
})
|
|
|
|
it('is Cache-Control: no-store on both branches', async () => {
|
|
const selfHost = await GET(createMockRequest('/x'), { params: Promise.resolve({}) })
|
|
expect(selfHost.headers.get('Cache-Control')).toBe('no-store')
|
|
selfHosted = false
|
|
const hosted = await GET(createMockRequest('/x'), { params: Promise.resolve({}) })
|
|
expect(hosted.headers.get('Cache-Control')).toBe('no-store')
|
|
})
|
|
|
|
it('echoes connect_url without userinfo, query or fragment', async () => {
|
|
vi.stubEnv('GNUBOK_CONNECTOR_KEY', 'gnubok_ck_x')
|
|
vi.stubEnv('GNUBOK_CONNECT_URL', 'https://op:secret@connect.example.se/?token=secret')
|
|
const { body } = await parseJsonResponse<{ data: { connect_url: string } }>(await GET(createMockRequest('/x'), { params: Promise.resolve({}) }))
|
|
expect(body.data.connect_url).toBe('https://connect.example.se')
|
|
})
|
|
|
|
it('reports own_credentials for an upstream configured directly on the instance', async () => {
|
|
vi.stubEnv('GNUBOK_CONNECTOR_KEY', 'gnubok_ck_x')
|
|
vi.stubEnv('ENABLE_BANKING_APP_ID', 'app')
|
|
const { body } = await parseJsonResponse<{ data: { upstreams: Record<string, string> } }>(await GET(createMockRequest('/x'), { params: Promise.resolve({}) }))
|
|
expect(body.data.upstreams.bank).toBe('own_credentials')
|
|
expect(body.data.upstreams.skatteverket).toBe('connector')
|
|
})
|
|
})
|