This commit is contained in:
Lars Nolden
2026-09-10 12:30:42 +02:00
commit 9843fe0c50
79 changed files with 16318 additions and 0 deletions
+106
View File
@@ -0,0 +1,106 @@
package app
import (
"context"
"errors"
"reflect"
"testing"
"time"
"finance-duck/internal/banking"
"finance-duck/internal/domain"
)
type bankScenario struct {
session banking.Session
fail bool
}
func (b *bankScenario) Authorize(context.Context, string, string, string) (string, error) {
return "https://bank.example/authorize", nil
}
func (b *bankScenario) Exchange(context.Context, string) (banking.Session, error) {
return b.session, nil
}
func (b *bankScenario) Status(context.Context, string) (banking.Session, error) {
if b.fail {
return banking.Session{}, errors.New("expired")
}
return b.session, nil
}
func (b *bankScenario) Balances(context.Context, string) ([]banking.Balance, error) {
return []banking.Balance{{Amount: "100.00", Currency: "EUR", Type: "CLBD"}}, nil
}
func (b *bankScenario) Transactions(_ context.Context, a domain.Account, from, to string) ([]domain.Facts, error) {
if b.fail {
return nil, errors.New("offline")
}
return []domain.Facts{{Source: "enablebanking", AccountID: a.ID, BookingDate: time.Now().UTC().AddDate(0, 0, -1).Format("2006-01-02"), Amount: "-42.80", Currency: "EUR", RawDescription: "REWE", ExternalID: "entry_stable"}}, nil
}
func TestSyncRestoresSavedConsentBindingsAndDoesNotDuplicateFacts(t *testing.T) {
a, s := testApp(t)
account := s.Data.Accounts[0]
account.ExternalAccountID = "provider_new"
account.IBAN = "DE89370400440532013000"
provider := &bankScenario{session: banking.Session{ID: "new_session", ValidUntil: time.Now().Add(24 * time.Hour).Format(time.RFC3339), Accounts: []domain.Account{account}}}
a.bank = provider
a.ops.Sessions = []banking.Session{provider.session}
if err := a.saveOps(); err != nil {
t.Fatal(err)
}
first, err := a.Sync(context.Background())
if err != nil {
t.Fatal(err)
}
if len(first.Data.Accounts) != 1 || first.Data.Accounts[0].ExternalAccountID != "provider_new" || len(first.Data.Transactions) != 1 {
t.Fatalf("saved session did not recover/import: %+v", first.Data)
}
again, err := a.Sync(context.Background())
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(first.Data, again.Data) {
t.Fatal("repeated bank synchronization changed canonical financial data")
}
provider.fail = true
failed, err := a.Sync(context.Background())
if err != nil {
t.Fatal(err)
}
if failed.Status.SyncError == "" || !reflect.DeepEqual(again.Data, failed.Data) {
t.Fatal("provider failure was not isolated from canonical data")
}
}
func TestReconnectReplacesOldConsentWithoutDuplicatingLocalAccount(t *testing.T) {
a, s := testApp(t)
account := s.Data.Accounts[0]
account.ExternalAccountID = "old_uid"
account.IBAN = "DE89370400440532013000"
s, err := a.Mutate(context.Background(), s.Revision, func(d *domain.Dataset) error { return SaveAccount(d, account) })
if err != nil {
t.Fatal(err)
}
a.ops.Sessions = []banking.Session{{ID: "old_session", Accounts: []domain.Account{account}}}
renewed := account
renewed.ID = "provider_local_id"
renewed.ExternalAccountID = "new_uid"
renewed.DisplayName = "Bank-generated name"
a.bank = &bankScenario{session: banking.Session{ID: "new_session", ValidUntil: time.Now().Add(24 * time.Hour).Format(time.RFC3339), Accounts: []domain.Account{renewed}}}
a.authStates["one_time_state"] = authorization{Expires: time.Now().Add(time.Minute), Institution: "N26", Country: "DE"}
if err = a.Callback(context.Background(), "bank_code", "one_time_state"); err != nil {
t.Fatal(err)
}
after, err := a.Snapshot(context.Background())
if err != nil {
t.Fatal(err)
}
if len(after.Data.Accounts) != 1 || after.Data.Accounts[0].ID != account.ID || after.Data.Accounts[0].DisplayName != account.DisplayName || after.Data.Accounts[0].ExternalAccountID != "new_uid" {
t.Fatal("reconnect duplicated account or lost local display name")
}
if len(after.Sessions) != 1 || after.Sessions[0].ID != "new_session" {
t.Fatal("expired session remains active after reconnect")
}
if err = a.Callback(context.Background(), "bank_code", "one_time_state"); err == nil {
t.Fatal("authorization state replay was accepted")
}
}