package web_test import ( "context" "fmt" "fuj-management/go/internal/config" "fuj-management/go/internal/domain/reconcile" "fuj-management/go/internal/web" "fuj-management/go/internal/web/api" "net/http" "net/http/httptest" "strings" "testing" ) // fixtureSources returns one adult ("Test Member", tier A) with a 2026-01 fee // of 750 CZK (4 sessions) and a matching payment of 750 — mirrors Python's // test_adults_route fixture. type fixtureSources struct{} func (fixtureSources) LoadAdults(_ context.Context) ([]reconcile.Member, []string, error) { return []reconcile.Member{ {Name: "Test Member", Tier: "A", Fees: map[string]reconcile.FeeData{ "2026-01": {Expected: 750, Attendance: 4}, }}, }, []string{"2026-01"}, nil } func (fixtureSources) LoadJuniors(_ context.Context) ([]reconcile.Member, []string, error) { return nil, nil, nil } func (fixtureSources) LoadTransactions(_ context.Context) ([]reconcile.Transaction, error) { amt := float64(750) return []reconcile.Transaction{ {Date: "2026-01-01", Amount: 750, Person: "Test Member", Purpose: "2026-01", InferredAmount: &amt}, }, nil } func (fixtureSources) LoadExceptions(_ context.Context) (map[reconcile.ExceptionKey]reconcile.Exception, error) { return nil, nil } func fixtureHandler(t *testing.T) *api.Handler { t.Helper() return &api.Handler{ Sources: fixtureSources{}, Config: config.Config{BankAccount: "CZ0000000000000000000000"}, } } func TestHTMLHandlerSmoke(t *testing.T) { renderer, err := web.NewRenderer() if err != nil { t.Fatalf("NewRenderer: %v", err) } b := web.BuildInfo{Version: "v0", Commit: "abc1234", BuildDate: "2026-01-01"} h := web.NewHTMLHandler(renderer, b, fixtureHandler(t)) cases := []struct { path string handler http.HandlerFunc }{ {"/adults", h.ServeAdults}, {"/juniors", h.ServeJuniors}, {"/payments", h.ServePayments}, {"/sync-bank", h.ServeSync}, {"/flush-cache", h.ServeFlushCache}, } for _, tc := range cases { t.Run(tc.path, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, tc.path, nil) w := httptest.NewRecorder() tc.handler(w, req) if w.Code != http.StatusOK { t.Errorf("status = %d, want 200", w.Code) } if ct := w.Header().Get("Content-Type"); !strings.HasPrefix(ct, "text/html") { t.Errorf("Content-Type = %q, want text/html", ct) } body := w.Body.String() if n := strings.Count(body, `class="active"`); n != 1 { t.Errorf(`class="active" count = %d, want 1`, n) } want := fmt.Sprintf(`href="%s" class="active"`, tc.path) if !strings.Contains(body, want) { t.Errorf("missing active link %q in body", want) } }) } } func TestAdultsPage(t *testing.T) { renderer, err := web.NewRenderer() if err != nil { t.Fatalf("NewRenderer: %v", err) } b := web.BuildInfo{Version: "v0", Commit: "abc1234", BuildDate: "2026-01-01"} h := web.NewHTMLHandler(renderer, b, fixtureHandler(t)) req := httptest.NewRequest(http.MethodGet, "/adults", nil) w := httptest.NewRecorder() h.ServeAdults(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200", w.Code) } body := w.Body.String() for _, want := range []string{ "Adults Dashboard", "Test Member", "750/750 CZK (4)", // paid/expected (attendance) } { if !strings.Contains(body, want) { t.Errorf("body missing %q", want) } } // Python assertion: cell text never says literally "OK" if strings.Contains(body, ">OK<") { t.Error("body should not contain >OK<") } } func TestModalMarkup(t *testing.T) { renderer, err := web.NewRenderer() if err != nil { t.Fatalf("NewRenderer: %v", err) } b := web.BuildInfo{Version: "v0", Commit: "abc1234", BuildDate: "2026-01-01"} h := web.NewHTMLHandler(renderer, b, fixtureHandler(t)) cases := []struct { path string handler http.HandlerFunc page string wantRow string // data-name present only when the page has rows }{ {"/adults", h.ServeAdults, "adults", `data-name="Test Member"`}, {"/juniors", h.ServeJuniors, "juniors", ""}, } for _, tc := range cases { t.Run(tc.path, func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, tc.path, nil) w := httptest.NewRecorder() tc.handler(w, req) body := w.Body.String() for _, want := range []string{ `data-page="` + tc.page + `"`, `id="memberModal"`, `/static/js/member-detail.js`, } { if !strings.Contains(body, want) { t.Errorf("body missing %q", want) } } if tc.wantRow != "" && !strings.Contains(body, tc.wantRow) { t.Errorf("body missing info-icon row %q", tc.wantRow) } }) } } func TestPaymentsPage(t *testing.T) { renderer, err := web.NewRenderer() if err != nil { t.Fatalf("NewRenderer: %v", err) } b := web.BuildInfo{Version: "v0", Commit: "abc1234", BuildDate: "2026-01-01"} h := web.NewHTMLHandler(renderer, b, fixtureHandler(t)) req := httptest.NewRequest(http.MethodGet, "/payments", nil) w := httptest.NewRecorder() h.ServePayments(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200", w.Code) } body := w.Body.String() for _, want := range []string{ "Payments Ledger", "

Test Member

", "750 CZK", "2026-01-01", "2026-01", } { if !strings.Contains(body, want) { t.Errorf("body missing %q", want) } } }