121 lines
5.3 KiB
Go
121 lines
5.3 KiB
Go
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)
|
|
}
|
|
}
|