package web // v0.67.0 — the two "the hub knows but never said so" banners on the customer page, plus the // self-bind auto-mint. Each test asserts the banner is ABSENT in the nominal case as well as // present in the deviating one: a banner that renders unconditionally is worse than none, because // operators stop reading it. import ( "net/http/httptest" "regexp" "strings" "testing" "time" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) const staleBannerMarker = "No data since the RESET" const offsiteBannerMarker = "Offsite is enabled but was never provisioned" func contains(html, needle string) bool { return strings.Contains(html, needle) } // renderCustomerPageWithQuery renders the page with a query string (the flash arrives that way). func renderCustomerPageWithQuery(t *testing.T, s *Server, customerID, query string) string { t.Helper() rr := httptest.NewRecorder() s.handleCustomerUnified(rr, httptest.NewRequest("GET", "/customers/"+customerID+query, nil), customerID) if rr.Code != 200 { t.Fatalf("customer page status = %d", rr.Code) } return rr.Body.String() } // flashSuccessBox captures the success flash div's inner content so a branch-less flash key // (which renders the div with nothing in it) can be told apart from one that has text. var flashSuccessBox = regexp.MustCompile(`(?s)
(.*?)
`) func flashBoxIsEmpty(html string) bool { m := flashSuccessBox.FindStringSubmatch(html) if m == nil { return true } return strings.TrimSpace(m[1]) == "" } func seedCustomer(t *testing.T, st *store.Store, id, configJSON string) { t.Helper() cfg := &store.CustomerConfig{ CustomerID: id, CustomerName: id, Domain: id + ".hu", RetrievalPassword: "pw", APIKey: "k", Status: "active", Email: id + "@example.com", } if configJSON != "" { cfg.ConfigJSON = configJSON } if err := st.SaveCustomerConfig(cfg); err != nil { t.Fatal(err) } } // --- R-37: post-RESET staleness ----------------------------------------------------------------- func TestCustomerPage_StaleSinceReset(t *testing.T) { t.Run("reset newer than the last report → banner", func(t *testing.T) { s, st := newTestServer(t) seedCustomer(t, st, "acme", "") if err := st.SaveReport("acme", []byte(tabsTestReportJSON)); err != nil { t.Fatal(err) } // A reset that STARTED and COMPLETED after that report. id, err := st.StartCustomerReset("acme", true) if err != nil { t.Fatal(err) } if err := st.FinishCustomerReset(id); err != nil { t.Fatal(err) } html := renderCustomerPage(t, s, "acme") if !contains(html, staleBannerMarker) { t.Error("a completed RESET newer than the newest report must raise the staleness banner — " + "every health figure on the page describes a lifecycle that no longer exists") } // The Hungarian phrasing the customer-facing surface will use is quoted for the operator. if !contains(html, "RESET óta nincs adat") { t.Error("banner should quote „RESET óta nincs adat\"") } }) t.Run("no reset at all → no banner", func(t *testing.T) { s, st := newTestServer(t) seedCustomer(t, st, "acme", "") if err := st.SaveReport("acme", []byte(tabsTestReportJSON)); err != nil { t.Fatal(err) } if html := renderCustomerPage(t, s, "acme"); contains(html, staleBannerMarker) { t.Error("a customer that was never reset must not show the staleness banner") } }) t.Run("reset still running (never completed) → no banner", func(t *testing.T) { s, st := newTestServer(t) seedCustomer(t, st, "acme", "") if err := st.SaveReport("acme", []byte(tabsTestReportJSON)); err != nil { t.Fatal(err) } if _, err := st.StartCustomerReset("acme", true); err != nil { // started, NOT finished t.Fatal(err) } if html := renderCustomerPage(t, s, "acme"); contains(html, staleBannerMarker) { t.Error("an in-flight reset must not claim the data is stale — only a COMPLETED one does") } }) t.Run("report arrived after the reset → banner clears", func(t *testing.T) { s, st := newTestServer(t) seedCustomer(t, st, "acme", "") id, err := st.StartCustomerReset("acme", true) if err != nil { t.Fatal(err) } if err := st.FinishCustomerReset(id); err != nil { t.Fatal(err) } // SQLite second-resolution timestamps: make the report unambiguously later. time.Sleep(1100 * time.Millisecond) if err := st.SaveReport("acme", []byte(tabsTestReportJSON)); err != nil { t.Fatal(err) } if html := renderCustomerPage(t, s, "acme"); contains(html, staleBannerMarker) { t.Error("once the box reports again the page is current — the banner must clear itself, " + "otherwise it becomes permanent furniture") } }) } // --- R-36 interim: offsite enabled but never provisioned ---------------------------------------- func TestCustomerPage_OffsiteUnprovisioned(t *testing.T) { cases := []struct { name string configJSON string wantBanner bool }{ { name: "enabled with no type → the silent state, banner", configJSON: `{"offsite":{"enabled":true}}`, wantBanner: true, }, { name: "enabled and provisioned → no banner", configJSON: `{"offsite":{"enabled":true,"type":"storagebox"}}`, wantBanner: false, }, { name: "disabled → no banner (nothing was promised)", configJSON: `{"offsite":{"enabled":false}}`, wantBanner: false, }, { name: "no offsite key at all → no banner", configJSON: `{}`, wantBanner: false, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { s, st := newTestServer(t) seedCustomer(t, st, "acme", tc.configJSON) html := renderCustomerPage(t, s, "acme") if got := contains(html, offsiteBannerMarker); got != tc.wantBanner { t.Errorf("banner present = %v, want %v (config %s)", got, tc.wantBanner, tc.configJSON) } }) } } // --- the pbsdr_reissued flash had no text at all (rendered an empty box) ------------------------- func TestCustomerPage_EveryRedirectFlashRendersText(t *testing.T) { // Every flash key the server can redirect with must produce visible text. pbsdr_reissued // shipped without a branch, so the operator got an empty success box after re-issuing PBS // credentials — observed live 2026-07-18. for _, flash := range []string{"pbsdr_reissued", "offsite_reissued", "reset_done", "created"} { t.Run(flash, func(t *testing.T) { s, st := newTestServer(t) seedCustomer(t, st, "acme", "") html := renderCustomerPageWithQuery(t, s, "acme", "?flash="+flash) if !contains(html, `class="flash flash-success"`) { t.Fatalf("flash %q rendered no success box at all", flash) } if flashBoxIsEmpty(html) { t.Errorf("flash %q renders an EMPTY box — the operator is shown a confirmation with no words", flash) } }) } }