107 lines
4.0 KiB
Go
107 lines
4.0 KiB
Go
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")
|
|
}
|
|
}
|