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>
158 lines
3.9 KiB
Go
158 lines
3.9 KiB
Go
package fio
|
||
|
||
import (
|
||
"context"
|
||
"io"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"os"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func TestAPIClient_ParseResponse(t *testing.T) {
|
||
body, err := os.ReadFile("testdata/api_response.json")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
txns, err := parseAPIResponse(body)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(txns) != 1 {
|
||
t.Fatalf("want 1 txn (outgoing filtered), got %d", len(txns))
|
||
}
|
||
tx := txns[0]
|
||
if tx.Date != "2026-04-10" {
|
||
t.Errorf("date: want '2026-04-10', got %q", tx.Date)
|
||
}
|
||
if tx.Amount != 750 {
|
||
t.Errorf("amount: want 750, got %v", tx.Amount)
|
||
}
|
||
if tx.Sender != "Jana Novakova" {
|
||
t.Errorf("sender: want 'Jana Novakova', got %q", tx.Sender)
|
||
}
|
||
if tx.Message != "duben 2026" {
|
||
t.Errorf("message: want 'duben 2026', got %q", tx.Message)
|
||
}
|
||
if tx.VS != "123" {
|
||
t.Errorf("vs: want '123', got %q", tx.VS)
|
||
}
|
||
if tx.BankID != "12345678901" {
|
||
t.Errorf("bank_id: want '12345678901', got %q", tx.BankID)
|
||
}
|
||
}
|
||
|
||
func TestAPIClient_HTTPRoundTrip(t *testing.T) {
|
||
body, _ := os.ReadFile("testdata/api_response.json")
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||
_, _ = w.Write(body)
|
||
}))
|
||
defer srv.Close()
|
||
|
||
c := &apiClient{token: "TESTTOKEN", hc: &overrideClient{base: srv.Client(), baseURL: srv.URL}}
|
||
txns, err := c.FetchTransactions(context.Background(), time.Now().AddDate(0, -1, 0), time.Now())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(txns) != 1 {
|
||
t.Fatalf("want 1 txn, got %d", len(txns))
|
||
}
|
||
}
|
||
|
||
func TestTransparentClient_ParseHTML(t *testing.T) {
|
||
body, err := os.ReadFile("testdata/transparent.html")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
txns, err := parseTransparentHTML(body)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// Only the incoming row (750 CZK) should be kept; -200 is outgoing
|
||
if len(txns) != 1 {
|
||
t.Fatalf("want 1 txn (outgoing filtered), got %d", len(txns))
|
||
}
|
||
tx := txns[0]
|
||
if tx.Date != "2026-04-10" {
|
||
t.Errorf("date: want '2026-04-10', got %q", tx.Date)
|
||
}
|
||
if tx.Amount != 750 {
|
||
t.Errorf("amount: want 750, got %v", tx.Amount)
|
||
}
|
||
if tx.Sender != "Jana Novakova" {
|
||
t.Errorf("sender: want 'Jana Novakova', got %q", tx.Sender)
|
||
}
|
||
if tx.VS != "123" {
|
||
t.Errorf("vs: want '123', got %q", tx.VS)
|
||
}
|
||
if tx.BankID != "" {
|
||
t.Errorf("bank_id: want empty on HTML path, got %q", tx.BankID)
|
||
}
|
||
}
|
||
|
||
func TestParseCzechDate(t *testing.T) {
|
||
cases := []struct{ in, want string }{
|
||
{"10.04.2026", "2026-04-10"},
|
||
{"10/04/2026", "2026-04-10"},
|
||
{"", ""},
|
||
{"invalid", ""},
|
||
}
|
||
for _, c := range cases {
|
||
if got := parseCzechDate(c.in); got != c.want {
|
||
t.Errorf("parseCzechDate(%q) = %q, want %q", c.in, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestParseCzechAmount(t *testing.T) {
|
||
cases := []struct {
|
||
in string
|
||
want float64
|
||
}{
|
||
{"750,00 CZK", 750},
|
||
{"1.500,00", 1500},
|
||
{"1500.00", 1500},
|
||
{"-200,00 CZK", -200},
|
||
}
|
||
for _, c := range cases {
|
||
if got := parseCzechAmount(c.in); got != c.want {
|
||
t.Errorf("parseCzechAmount(%q) = %v, want %v", c.in, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestFake(t *testing.T) {
|
||
f := &Fake{Transactions: []Transaction{{Date: "2026-04-01", Amount: 500}}}
|
||
txns, err := f.FetchTransactions(context.Background(), time.Now(), time.Now())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(txns) != 1 || txns[0].Date != "2026-04-01" {
|
||
t.Errorf("unexpected: %v", txns)
|
||
}
|
||
}
|
||
|
||
// overrideClient replaces the URL in requests so we can hit a local test server
|
||
// instead of the real Fio URL.
|
||
type overrideClient struct {
|
||
base *http.Client
|
||
baseURL string
|
||
}
|
||
|
||
func (o *overrideClient) Do(req *http.Request) (*http.Response, error) {
|
||
r2, _ := http.NewRequestWithContext(req.Context(), req.Method, o.baseURL+req.URL.Path, nil)
|
||
resp, err := o.base.Do(r2)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// The api client reads the body, so re-serve whatever the test server returned.
|
||
return resp, nil
|
||
}
|
||
|
||
// verify Fake satisfies Client
|
||
var _ Client = (*Fake)(nil)
|
||
|
||
// ensure io.ReadAll isn't called at top level (compile-time reference suppressor)
|
||
var _ = io.ReadAll
|