All checks were successful
Deploy to K8s / deploy (push) Successful in 10s
- Add web/api/handler.go: Handler struct wiring Sources+Config into ServeAdults, ServeJuniors, ServePayments, ServeVersion - Add web/api/build_common.go: getMonthLabels, groupRawPaymentsByPerson, settledBalance, domain-to-wire converters, ensureSlice generic helper - Add web/api/build_adults.go: buildAdultsResponse + buildAdultMemberRow mirroring scripts/views.py:build_adults_view_model - Add web/api/build_juniors.go: buildJuniorsResponse + buildJuniorMemberRow mirroring scripts/views.py:build_juniors_view_model, including "?" sentinel and :NJ,MA breakdown - Add web/api/build_payments.go: buildPaymentsResponse with Unmatched/Unknown bucket - Extend reconcile.FeeData/MonthData with IsUnknown, JuniorAttendance, AdultAttendance - Extend reconcile.Transaction with ManualFix, VS, BankID, SyncID for raw_payments wire field - Export membership.AdultMergedMonths and JuniorMergedMonths - Update sources.go to propagate new FeeData fields and parse extra transaction columns - Wire sources+cfg into web.Run; register /api/* routes via Go 1.22 method+path patterns - Fix pre-existing gofumpt formatting in fio_test.go and fio_table.go Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
179 lines
4.8 KiB
Go
179 lines
4.8 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"},
|
||
{"7.5.2026", "2026-05-07"}, // non-padded — real Fio transparent page format
|
||
{"3.12.2025", "2025-12-03"}, // non-padded single-digit day, double-digit month
|
||
{"07.05.26", "2026-05-07"}, // padded 2-digit year — current Fio transparent page format
|
||
{"7.5.26", "2026-05-07"}, // non-padded 2-digit year
|
||
{"07/05/26", "2026-05-07"}, // slash variant
|
||
{"", ""},
|
||
{"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 TestExtractSecondTableRows_NestedTable(t *testing.T) {
|
||
// Regression: a nested <table> inside the target must not cause early exit.
|
||
html := `<table class="table"><tr><td>nav</td></tr></table>
|
||
<table class="table">
|
||
<thead><tr><th>Date</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>7.5.2026</td><td><table><tr><td>nested</td></tr></table></td></tr>
|
||
<tr><td>6.5.2026</td><td></td></tr>
|
||
</tbody>
|
||
</table>`
|
||
rows := extractSecondTableRows([]byte(html))
|
||
if len(rows) != 2 {
|
||
t.Errorf("want 2 data rows, got %d: %v", len(rows), rows)
|
||
}
|
||
}
|
||
|
||
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
|