package web // v0.70.1 — the ghost customer's Delete button must EXIST. v0.70.0 made ghost customers // deletable server-side, but the Danger-zone card sat inside {{if .HasConfig}}, so the whole // ghost path was implemented and unreachable (dead UI — the fourth inert-seam defect, found // live on demo-vm-felhom). Handler tests that POST directly prove nothing about reachability: // these tests assert the RENDERED page, per branch of the new gate. // // Assertions anchor on the delete form's action attribute and the customerDeleteOpen( call // site — not free-text strings a copy edit would break. import ( "net/http/httptest" "strings" "testing" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) const ( deleteFormMarker = `action="/configs/ghost/delete"` deleteOpenMarker = `customerDeleteOpen('ghost')` deleteFuncMarker = `function customerDeleteOpen(` resetFormMarker = `action="/configs/ghost/reset"` blockFormMarker = `action="/customers/ghost/block"` unblockFormMkr = `action="/customers/ghost/unblock"` ) // Scenario A — ghost with residue (the demo-vm-felhom shape): no config row, 0 hosts, // residue.Total() > 0 (report rows). The Danger zone must render — delete form, opener button, // and its script — while RESET and Block (config-coupled affordances) must NOT. func TestCustomerPage_GhostWithResidue_RendersDeleteOnly(t *testing.T) { s, st := newTestServer(t) // Reports only — NO SaveCustomerConfig. This is the ghost: config gone, residue keeps the // customer on the list and raising staleness alerts. if err := st.SaveReport("ghost", []byte(tabsTestReportJSON)); err != nil { t.Fatal(err) } if residue, err := st.CustomerResidue("ghost"); err != nil || residue.Total() == 0 { t.Fatalf("fixture must have residue (got total=%v err=%v)", residue, err) } html := renderCustomerPage(t, s, "ghost") if !strings.Contains(html, deleteFormMarker) { t.Errorf("ghost page must render the delete form (%s) — without it the entire v0.70.0 ghost-delete path is dead UI", deleteFormMarker) } if !strings.Contains(html, deleteOpenMarker) { t.Errorf("ghost page must render the Delete button's %s opener", deleteOpenMarker) } if !strings.Contains(html, deleteFuncMarker) { t.Errorf("the customerDeleteOpen script must exist whenever the button does (was inside the old HasConfig gate)") } // The WRONG outcome this split must not introduce: RESET rendering for a ghost. RESET is // identity-preserving re-onboarding; a ghost has no identity to preserve. if strings.Contains(html, resetFormMarker) { t.Errorf("RESET card must NOT render for a ghost (found %s)", resetFormMarker) } // Blocking gates dashboard visibility of a CONFIGURED customer — meaningless for a ghost. if strings.Contains(html, blockFormMarker) || strings.Contains(html, unblockFormMkr) { t.Error("Block/Unblock forms must NOT render for a ghost") } } // Scenario B — normal configured customer: byte-for-byte the affordances of today — RESET card // + full Danger zone incl. Block, and the delete machinery. func TestCustomerPage_ConfiguredCustomer_KeepsAllAffordances(t *testing.T) { s, st := newTestServer(t) if err := st.SaveCustomerConfig(&store.CustomerConfig{ CustomerID: "ghost", CustomerName: "Ghost", Domain: "ghost.hu", RetrievalPassword: "pw", APIKey: "k", Status: "active", }); err != nil { t.Fatal(err) } if err := st.SaveReport("ghost", []byte(tabsTestReportJSON)); err != nil { t.Fatal(err) } html := renderCustomerPage(t, s, "ghost") for _, marker := range []string{resetFormMarker, blockFormMarker, deleteFormMarker, deleteOpenMarker, deleteFuncMarker} { if !strings.Contains(html, marker) { t.Errorf("configured customer lost an affordance: %s absent — the gate split must not change the normal shape", marker) } } } // Scenario B variant — a BLOCKED configured customer renders Unblock (the IsBlocked branch // survived the extra HasConfig wrapper around the block forms). func TestCustomerPage_BlockedCustomer_RendersUnblock(t *testing.T) { s, st := newTestServer(t) if err := st.SaveCustomerConfig(&store.CustomerConfig{ CustomerID: "ghost", CustomerName: "Ghost", Domain: "ghost.hu", RetrievalPassword: "pw", APIKey: "k", Status: "active", }); err != nil { t.Fatal(err) } // SaveCustomerConfig never writes status — blocking goes through SetCustomerConfigStatus // (the same call the /block handler makes). if err := st.SetCustomerConfigStatus("ghost", "blocked"); err != nil { t.Fatal(err) } html := renderCustomerPage(t, s, "ghost") if !strings.Contains(html, unblockFormMkr) { t.Errorf("blocked configured customer must render the Unblock form (%s)", unblockFormMkr) } } // Scenario C — nothing left: no config, 0 hosts, residue.Total() == 0. There is no renderable // Deletable=false state with real store reads (customer != nil implies a report row implies // residue > 0), so "no Danger zone" manifests as the page-level 404 — the same semantics as the // delete preview's 404 ("there is nothing here"). func TestCustomerPage_NothingLeft_404(t *testing.T) { s, _ := newTestServer(t) rr := httptest.NewRecorder() s.handleCustomerUnified(rr, httptest.NewRequest("GET", "/customers/ghost", nil), "ghost") if rr.Code != 404 { t.Fatalf("a customer with no config, no hosts and no residue must 404 (mirrors the preview) — got %d", rr.Code) } }