Files
fuj-management/go/internal/services/banksync/sync_test.go
T
kacerrandClaude Opus 5.5 62c6dc2ca0 fix(sync): dashboard sync covers current year plus Nov-Dec of previous
/sync-bank now uses one shared default window in both implementations:
1 November of the previous year to 31 December of the current year.
Payments dated around New Year are picked up in January; duplicates
are still filtered by Sync ID.

Python used a hardcoded 2026 window and would have stopped syncing on
2027-01-01; Go used the current calendar year only.

Adds make sync-year [YEAR=N] with the same window. The CLI default
without flags (last 30 days) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 12:04:59 +02:00

216 lines
6.9 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", []fio.Client{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])
}
// VS (index 7) is written as text so Sheets keeps it a string.
if vs, _ := rows[0][7].(string); vs != "'123" {
t.Errorf("expected VS written as text cell '123, got %v", rows[0][7])
}
}
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", []fio.Client{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", []fio.Client{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", []fio.Client{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", []fio.Client{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", []fio.Client{fioFake}, sh, SyncOpts{From: from, To: to})
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Errorf("want 1 row, got %d", n)
}
}
func TestSyncToSheets_DryRun(t *testing.T) {
sh := &sheets.Fake{Values: map[string][][]any{"SHEETID/A1:K": {}}}
fioFake := &fio.Fake{Transactions: testFioTxns}
n, err := SyncToSheets(context.Background(), "SHEETID", []fio.Client{fioFake}, sh,
SyncOpts{Days: 30, Sort: true, DryRun: true})
if err != nil {
t.Fatal(err)
}
if n != 2 {
t.Errorf("want 2 planned, got %d", n)
}
if len(sh.Appended) != 0 {
t.Error("dry-run must not call AppendValues")
}
}
func TestSyncToSheets_MultiAccount(t *testing.T) {
txnsA := []fio.Transaction{
{Date: "2026-04-10", Amount: 700, Sender: "Alice", Message: "april", VS: "1", BankID: "A1"},
}
txnsB := []fio.Transaction{
{Date: "2026-04-11", Amount: 500, Sender: "Bob", Message: "duben", VS: "2", BankID: "B1"},
}
// One transaction that duplicates the first one from account A (same sync_id).
dupID := syncIDFor(txnsA[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", 700.0, "", "", "", "", "Alice", "1", "april", "A1", dupID},
},
}}
fakeA := &fio.Fake{Transactions: txnsA}
fakeB := &fio.Fake{Transactions: txnsB}
n, err := SyncToSheets(context.Background(), "SHEETID", []fio.Client{fakeA, fakeB}, sh, SyncOpts{Days: 30})
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Errorf("want 1 new row (B1 from account B; A1 is duplicate), got %d", n)
}
if len(sh.Appended) != 1 || len(sh.Appended[0].Rows) != 1 {
t.Fatalf("want exactly 1 row appended, got %v", sh.Appended)
}
row := sh.Appended[0].Rows[0]
if row[6] != "Bob" {
t.Errorf("expected Bob's row, got sender=%v", row[6])
}
}
// 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,
})
}
func TestDefaultWindow(t *testing.T) {
cases := []struct {
now time.Time
from, to string
}{
{time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC), "2025-11-01", "2026-12-31"},
{time.Date(2027, 1, 1, 0, 0, 0, 0, time.UTC), "2026-11-01", "2027-12-31"},
{time.Date(2027, 12, 31, 23, 0, 0, 0, time.UTC), "2026-11-01", "2027-12-31"},
}
for _, tc := range cases {
from, to := DefaultWindow(tc.now)
if got := from.Format("2006-01-02"); got != tc.from {
t.Errorf("DefaultWindow(%s) from: want %s, got %s", tc.now.Format("2006-01-02"), tc.from, got)
}
if got := to.Format("2006-01-02"); got != tc.to {
t.Errorf("DefaultWindow(%s) to: want %s, got %s", tc.now.Format("2006-01-02"), tc.to, got)
}
}
}