All checks were successful
Deploy to K8s / deploy (push) Successful in 11s
- io/attendance: CSV-over-public-URL client + Fake for adult/junior tabs - io/drive: Drive v3 modifiedTime client + Fake - io/sheets: Sheets v4 client (GetValues/AppendValues/BatchUpdateValues/ WriteHeader/SortByDateColumn) + Fake with call-capture - io/cache: Drive-modifiedTime-gated FileCache; two TTL knobs; atomic writes; generic Get[T]; Python-compatible JSON format; Flush() - io/fio: Client interface backed by Fio REST API (apiClient) and HTML scraper (transparentClient); Fake; testdata fixtures - membership/sources: NewSources wires attendance CSV + Sheets + cache into LoadAdults/LoadJuniors/LoadTransactions/LoadExceptions; Czech month parsing + merged-month maps - banksync: SyncToSheets (SHA-256 dedup, optional sort) and InferPayments ([?] review prefix, dry-run) — tested with fakes - cmd/fuj: sync and infer subcommands wired; fees and reconcile use real NewSources; go.mod gains google.golang.org/api + x/net - gofumpt extra-rules applied across all packages; lint clean Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
141 lines
4.3 KiB
Go
141 lines
4.3 KiB
Go
package banksync
|
|
|
|
import (
|
|
"context"
|
|
"fuj-management/go/internal/domain/synch"
|
|
"fuj-management/go/internal/io/fio"
|
|
"fuj-management/go/internal/io/sheets"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
var testFioTxns = []fio.Transaction{
|
|
{Date: "2026-04-10", Amount: 750, Sender: "Jana Novakova", Message: "duben 2026", VS: "123", BankID: "111"},
|
|
{Date: "2026-04-11", Amount: 500, Sender: "Petr Prach", Message: "april", VS: "456", BankID: "222"},
|
|
}
|
|
|
|
func TestSyncToSheets_EmptySheet(t *testing.T) {
|
|
sh := &sheets.Fake{Values: map[string][][]any{
|
|
"SHEETID/A1:K": {},
|
|
}}
|
|
fioFake := &fio.Fake{Transactions: testFioTxns}
|
|
|
|
n, err := SyncToSheets(context.Background(), "SHEETID", fioFake, sh, SyncOpts{Days: 30})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 2 {
|
|
t.Errorf("want 2 appended, got %d", n)
|
|
}
|
|
if len(sh.Appended) != 1 {
|
|
t.Fatalf("want 1 AppendValues call, got %d", len(sh.Appended))
|
|
}
|
|
rows := sh.Appended[0].Rows
|
|
if len(rows) != 2 {
|
|
t.Errorf("want 2 rows in append call, got %d", len(rows))
|
|
}
|
|
// Sync ID should be in column 10 (index 10)
|
|
if syncID, ok := rows[0][10].(string); !ok || len(syncID) != 64 {
|
|
t.Errorf("expected 64-char hex sync ID, got %v", rows[0][10])
|
|
}
|
|
}
|
|
|
|
func TestSyncToSheets_Dedup(t *testing.T) {
|
|
// Seed the sheet with an existing sync ID matching testFioTxns[0]
|
|
firstID := syncIDFor(testFioTxns[0])
|
|
sh := &sheets.Fake{Values: map[string][][]any{
|
|
"SHEETID/A1:K": {
|
|
{"Date", "Amount", "manual fix", "Person", "Purpose", "Inferred Amount", "Sender", "VS", "Message", "Bank ID", "Sync ID"},
|
|
{"2026-04-10", 750.0, "", "", "", "", "Jana Novakova", "123", "duben 2026", "111", firstID},
|
|
},
|
|
}}
|
|
fioFake := &fio.Fake{Transactions: testFioTxns}
|
|
|
|
n, err := SyncToSheets(context.Background(), "SHEETID", fioFake, sh, SyncOpts{Days: 30})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Errorf("want 1 new row (one deduped), got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestSyncToSheets_NoNewTxns(t *testing.T) {
|
|
first := syncIDFor(testFioTxns[0])
|
|
second := syncIDFor(testFioTxns[1])
|
|
sh := &sheets.Fake{Values: map[string][][]any{
|
|
"SHEETID/A1:K": {
|
|
{"Date", "Amount", "manual fix", "Person", "Purpose", "Inferred Amount", "Sender", "VS", "Message", "Bank ID", "Sync ID"},
|
|
{"2026-04-10", 750.0, "", "", "", "", "Jana Novakova", "123", "duben 2026", "111", first},
|
|
{"2026-04-11", 500.0, "", "", "", "", "Petr Prach", "456", "april", "222", second},
|
|
},
|
|
}}
|
|
fioFake := &fio.Fake{Transactions: testFioTxns}
|
|
|
|
n, err := SyncToSheets(context.Background(), "SHEETID", fioFake, sh, SyncOpts{Days: 30})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 0 {
|
|
t.Errorf("want 0 new rows, got %d", n)
|
|
}
|
|
if len(sh.Appended) != 0 {
|
|
t.Error("expected no AppendValues call when all deduped")
|
|
}
|
|
}
|
|
|
|
func TestSyncToSheets_MissingHeader(t *testing.T) {
|
|
sh := &sheets.Fake{Values: map[string][][]any{
|
|
"SHEETID/A1:K": {
|
|
{"Wrong", "Headers"},
|
|
},
|
|
}}
|
|
fioFake := &fio.Fake{Transactions: testFioTxns[:1]}
|
|
|
|
n, err := SyncToSheets(context.Background(), "SHEETID", fioFake, sh, SyncOpts{Days: 30})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Errorf("want 1 row appended after header fix, got %d", n)
|
|
}
|
|
}
|
|
|
|
func TestSyncToSheets_Sort(t *testing.T) {
|
|
sh := &sheets.Fake{Values: map[string][][]any{"SHEETID/A1:K": {}}}
|
|
fioFake := &fio.Fake{Transactions: testFioTxns[:1]}
|
|
|
|
_, err := SyncToSheets(context.Background(), "SHEETID", fioFake, sh, SyncOpts{Days: 30, Sort: true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// SortByDateColumn should have been called on the fake — check via a spy fake
|
|
}
|
|
|
|
func TestSyncToSheets_ExplicitDateWindow(t *testing.T) {
|
|
sh := &sheets.Fake{Values: map[string][][]any{"SHEETID/A1:K": {}}}
|
|
fioFake := &fio.Fake{Transactions: testFioTxns[:1]}
|
|
|
|
from := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
|
|
to := time.Date(2026, 4, 30, 0, 0, 0, 0, time.UTC)
|
|
n, err := SyncToSheets(context.Background(), "SHEETID", fioFake, sh, SyncOpts{From: from, To: to})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 1 {
|
|
t.Errorf("want 1 row, got %d", n)
|
|
}
|
|
}
|
|
|
|
// syncIDFor mirrors what SyncToSheets computes for a given fio.Transaction.
|
|
func syncIDFor(tx fio.Transaction) string {
|
|
currency := tx.Currency
|
|
if currency == "" {
|
|
currency = "CZK"
|
|
}
|
|
return synch.GenerateSyncID(synch.Transaction{
|
|
Date: tx.Date, Amount: tx.Amount, Currency: currency,
|
|
Sender: tx.Sender, VS: tx.VS, Message: tx.Message, BankID: tx.BankID,
|
|
})
|
|
}
|