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)
}
})
}
}