Files
accounted/extensions/general/enable-banking/lib/__tests__/connection-warning.test.ts
T
MattssonandClaude Fable 5 7993700589 fix(enable-banking): only hard-warn about sibling connections at observed one-session banks (#2005)
* fix(enable-banking): only hard-warn about sibling connections at observed one-session banks

The same-bank warning dialog fired for every bank whenever the user's other
companies held a connection, claiming the siblings could stop syncing. That is
only true for banks binding one active AIS session per PSU (observed: SEB).
Prod shows Handelsbanken tolerates at least four concurrent sessions syncing
daily, and the generic warning made a multi-company user abandon a legitimate
renewal of an expired Handelsbanken connection.

The decision now lives in a pure, tested module (connection-warning.ts):

- One-session banks (SEB): hard warning on fresh connect and renewal alike.
- Other banks, renewal: no dialog; the connection already coexisted.
- Other banks, fresh connect: calm confirmation so a user who meant to renew
  notices they are about to create a second connection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PUKATngZZzqkoDesyxKR3

* fix(enable-banking): fail closed for banks with unknown session policy, exempt shared-session siblings

Skeptic findings on the first cut:

- Unknown banks were treated like verified multi-session banks (silent
  renewal), turning absence of evidence into evidence of absence. They now
  keep the previous hedged warning on both paths; only Handelsbanken
  (verified 2026-08-28: 2-4 concurrent connections per user on DISTINCT
  session_ids, all syncing daily) gets the calm tier.
- Siblings sharing the session being renewed are carried by
  fanOutSessionRenewal and never break, so they no longer trigger or
  inflate the warning; the absolute SEB claim is accurate for the
  remaining, genuinely separate sessions.
- Company names are deduped and phrasing follows distinct company count
  (one company holding privat + foretag rows is "ett annat bolag").
- Null bank_name no longer throws (DB column is nullable).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PUKATngZZzqkoDesyxKR3

* fix(enable-banking): count clashing companies by id, names are display-only

CodeRabbit: two distinct companies sharing a name, or one whose name fails
to resolve, undercounted to "ett annat bolag". SameBankClash now carries
companyId as identity; the parenthetical name list stays deduped display.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PUKATngZZzqkoDesyxKR3

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 15:31:38 +02:00

195 lines
6.9 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
isOneSessionBank,
isVerifiedMultiSessionBank,
sameBankWarning,
type SameBankClash,
} from '../connection-warning'
function clash(over: Partial<SameBankClash> = {}): SameBankClash {
return {
companyId: 'company-1',
companyName: 'Testbrand AB',
sessionId: 'sess-other',
...over,
}
}
describe('bank tier matching', () => {
it('matches SEB regardless of casing and whitespace', () => {
expect(isOneSessionBank('SEB')).toBe(true)
expect(isOneSessionBank(' seb ')).toBe(true)
})
it('matches Handelsbanken as verified multi-session', () => {
expect(isVerifiedMultiSessionBank('Handelsbanken')).toBe(true)
expect(isVerifiedMultiSessionBank('handelsbanken')).toBe(true)
})
it('does not match banks merely containing the letters, nor null', () => {
expect(isOneSessionBank('SEB Kort Bank')).toBe(false)
expect(isOneSessionBank(null)).toBe(false)
expect(isOneSessionBank(undefined)).toBe(false)
expect(isVerifiedMultiSessionBank('Nordea')).toBe(false)
})
})
describe('sameBankWarning', () => {
const base = {
bankName: 'Handelsbanken',
clashes: [clash()],
isReconnect: false,
}
it('returns null when nothing clashes', () => {
expect(sameBankWarning({ ...base, clashes: [] })).toBeNull()
})
it('is silent when renewing at a verified multi-session bank', () => {
// The regression this module fixes: a user abandoned a legitimate
// Handelsbanken renewal because the old dialog warned about sibling
// connections that HB demonstrably tolerates.
expect(sameBankWarning({ ...base, isReconnect: true })).toBeNull()
})
it('calmly confirms a fresh second connection at a verified multi-session bank', () => {
const warning = sameBankWarning(base)
expect(warning).not.toBeNull()
expect(warning?.confirmLabel).toBe('Anslut')
expect(warning?.description).not.toContain('sluta synka')
expect(warning?.description).toContain('Testbrand AB')
})
it('hard-warns for a one-session bank on fresh connect and reconnect alike', () => {
// A renewal also mints a new authorization, which at a one-session bank
// revokes the sibling companies' session just like a fresh connect does.
for (const isReconnect of [false, true]) {
const warning = sameBankWarning({ ...base, bankName: 'SEB', isReconnect })
expect(warning?.title).toBe('Du har redan 1 anslutning till SEB')
expect(warning?.description).toContain('slutar de andra att synka')
expect(warning?.confirmLabel).toBe('Fortsätt ändå')
}
})
it('keeps the hedged warning for banks with unknown session policy, on both paths', () => {
// Fail closed: absence of evidence about a bank is not evidence that it
// tolerates parallel sessions, so unknown banks keep the old behavior.
for (const isReconnect of [false, true]) {
const warning = sameBankWarning({ ...base, bankName: 'Nordea', isReconnect })
expect(warning).not.toBeNull()
expect(warning?.description).toContain('Vissa banker tillåter bara en aktiv anslutning')
expect(warning?.description).toContain('kan de andra sluta synka')
expect(warning?.confirmLabel).toBe('Fortsätt')
}
})
it('exempts siblings sharing the session being renewed', () => {
// fanOutSessionRenewal carries the renewed session to every sibling on
// it, so they never break and must not trigger the warning: even at SEB,
// even at an unknown bank.
for (const bankName of ['SEB', 'Nordea']) {
const warning = sameBankWarning({
bankName,
clashes: [clash({ sessionId: 'sess-current' })],
isReconnect: true,
currentSessionId: 'sess-current',
})
expect(warning).toBeNull()
}
})
it('still warns about siblings on OTHER sessions when renewing', () => {
const warning = sameBankWarning({
bankName: 'SEB',
clashes: [
clash({ sessionId: 'sess-current', companyName: 'Delad AB' }),
clash({ sessionId: 'sess-other', companyName: 'Separat AB' }),
],
isReconnect: true,
currentSessionId: 'sess-current',
})
expect(warning?.title).toBe('Du har redan 1 anslutning till SEB')
expect(warning?.description).toContain('Separat AB')
expect(warning?.description).not.toContain('Delad AB')
})
it('never exempts on null session ids', () => {
// A null on either side proves nothing about sharing.
const warning = sameBankWarning({
bankName: 'SEB',
clashes: [clash({ sessionId: null })],
isReconnect: true,
currentSessionId: null,
})
expect(warning).not.toBeNull()
})
it('dedupes company names and phrases by company count', () => {
// One company can hold two connections (privat + företag) to one bank:
// that is still "ett annat bolag", named once.
const warning = sameBankWarning({
bankName: 'SEB',
clashes: [
clash({ sessionId: 'a' }),
clash({ sessionId: 'b' }),
],
isReconnect: false,
})
expect(warning?.title).toBe('Du har redan 2 anslutningar till SEB')
expect(warning?.description).toContain('ett annat bolag (Testbrand AB)')
expect(warning?.description).not.toContain('Testbrand AB, Testbrand AB')
})
it('pluralizes across distinct companies', () => {
const warning = sameBankWarning({
bankName: 'SEB',
clashes: [
clash({ companyName: 'Testbrand AB' }),
clash({ companyId: 'company-2', companyName: 'Provbolaget AB', sessionId: 'sess-2' }),
],
isReconnect: false,
})
expect(warning?.description).toContain('andra bolag (Testbrand AB, Provbolaget AB)')
})
it('counts companies by id, not display name', () => {
// Two DISTINCT companies sharing a name, or a company whose name failed
// to resolve, must still pluralize: names are display-only.
const sameName = sameBankWarning({
bankName: 'SEB',
clashes: [
clash({ companyId: 'company-1', companyName: 'Testbrand AB' }),
clash({ companyId: 'company-2', companyName: 'Testbrand AB', sessionId: 'sess-2' }),
],
isReconnect: false,
})
expect(sameName?.description).toContain('andra bolag (Testbrand AB)')
const mixedNamedUnnamed = sameBankWarning({
bankName: 'SEB',
clashes: [
clash({ companyId: 'company-1', companyName: 'Testbrand AB' }),
clash({ companyId: 'company-2', companyName: null, sessionId: 'sess-2' }),
],
isReconnect: false,
})
expect(mixedNamedUnnamed?.description).toContain('andra bolag (Testbrand AB)')
})
it('omits the company list when no names are known', () => {
const warning = sameBankWarning({
...base,
bankName: 'SEB',
clashes: [clash({ companyName: null })],
})
expect(warning?.description).not.toContain('(')
})
it('survives a null bank name', () => {
const warning = sameBankWarning({ ...base, bankName: null })
expect(warning).not.toBeNull()
expect(warning?.title).toContain('Banken')
})
})