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
{"", ""},
{"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
inside the target must not cause early exit.
html := `
`
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