ea0d3f1764
The catalog knew only 'present' or 'gone', and 'gone' orphans every customer already running the app. lifecycle: in .felhom.yml withdraws an app from new installs without touching anyone running it. Deploy gate is server-side and fail-closed, before any mutation, with the ruled Hungarian refusal - hiding a button is not a gate. Unknown values fail OPEN (available + one WARN), deliberately opposite, so a typo or a newer catalog cannot pull a working app out of every customer's list. Orphan detection never sees the field - a red-proof adds that filter and shows the abandoned app immediately reading as an orphan. Badge plumbing is generic (MetaBadge + meta_badge partial) so R-56's difficulty labels drop in with no new markup.
139 lines
5.0 KiB
Go
139 lines
5.0 KiB
Go
package web
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
|
)
|
|
|
|
func lcStack(name, lifecycle string, deployed bool) stacks.Stack {
|
|
return stacks.Stack{
|
|
Name: name,
|
|
Deployed: deployed,
|
|
State: stacks.StateNotDeployed,
|
|
Meta: stacks.Metadata{DisplayName: name, Slug: name, Lifecycle: lifecycle},
|
|
}
|
|
}
|
|
|
|
// TestVisibleCatalogStacks pins the catalog-listing rule on both axes at once: withdrawn templates
|
|
// disappear from the OFFER list, and a deployed instance of the very same template stays.
|
|
//
|
|
// COMPANION RED-PROOF: drop the `st.Deployed ||` clause and the two "deployed" rows fail — a
|
|
// customer's working app vanishes from their own Alkalmazások page, which is the failure that makes
|
|
// withdrawing an app dangerous in the first place.
|
|
func TestVisibleCatalogStacks(t *testing.T) {
|
|
in := []stacks.Stack{
|
|
lcStack("bookstack", "", false), // available, not deployed → OFFERED
|
|
lcStack("immich", "", true), // available, deployed → shown
|
|
lcStack("plant-it", "abandoned", false), // withdrawn, not deployed → HIDDEN
|
|
lcStack("plant-it-run", "abandoned", true),// withdrawn, DEPLOYED → shown
|
|
lcStack("oldapp", "hidden", false), // withdrawn, not deployed → HIDDEN
|
|
lcStack("oldapp-run", "hidden", true), // withdrawn, DEPLOYED → shown
|
|
lcStack("typoapp", "bogus", false), // unknown → available → OFFERED (fail-open)
|
|
}
|
|
got := map[string]bool{}
|
|
for _, st := range visibleCatalogStacks(in) {
|
|
got[st.Name] = true
|
|
}
|
|
want := []string{"bookstack", "immich", "plant-it-run", "oldapp-run", "typoapp"}
|
|
notWant := []string{"plant-it", "oldapp"}
|
|
|
|
for _, n := range want {
|
|
if !got[n] {
|
|
t.Errorf("%q must be listed", n)
|
|
}
|
|
}
|
|
for _, n := range notWant {
|
|
if got[n] {
|
|
t.Errorf("%q is withdrawn and not deployed — it must NOT be offered", n)
|
|
}
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Errorf("listed %d stacks, want %d (%v)", len(got), len(want), got)
|
|
}
|
|
}
|
|
|
|
// TestVisibleCatalogStacks_ProtectedAlwaysSurvives: infra stacks carry no catalog metadata and must
|
|
// never be filtered out by a rule about catalog apps.
|
|
func TestVisibleCatalogStacks_ProtectedAlwaysSurvives(t *testing.T) {
|
|
st := lcStack("traefik", "hidden", false)
|
|
st.Protected = true
|
|
if len(visibleCatalogStacks([]stacks.Stack{st})) != 1 {
|
|
t.Fatal("a protected infra stack must never be hidden by the lifecycle filter")
|
|
}
|
|
}
|
|
|
|
func TestLifecycleBadge(t *testing.T) {
|
|
cases := []struct {
|
|
lifecycle string
|
|
wantBadge bool
|
|
}{
|
|
{"", false},
|
|
{"available", false},
|
|
// hidden renders NOTHING on a deployed app: "we stopped offering this" is not a fact the
|
|
// customer running it needs, and a badge implying something is wrong would be misleading.
|
|
{"hidden", false},
|
|
{"abandoned", true},
|
|
{"bogus", false},
|
|
}
|
|
for _, c := range cases {
|
|
b := lifecycleBadge(stacks.Metadata{Lifecycle: c.lifecycle})
|
|
if (b != nil) != c.wantBadge {
|
|
t.Errorf("lifecycle %q: badge=%v, want present=%v", c.lifecycle, b, c.wantBadge)
|
|
}
|
|
if b != nil {
|
|
if b.Label != "Nem karbantartott" {
|
|
t.Errorf("label = %q", b.Label)
|
|
}
|
|
if b.Title == "" {
|
|
t.Error("a badge that is only a word is a riddle — it must carry an explanation")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAbandonedBadgeRendersOnCatalogCard renders the PRODUCTION stacks template.
|
|
//
|
|
// COMPANION RED-PROOF: remove the {{template "meta_badge" ...}} line from stacks.html and this
|
|
// fails — the badge is the only thing telling a customer the software is no longer maintained.
|
|
func TestAbandonedBadgeRendersOnCatalogCard(t *testing.T) {
|
|
deployed := lcStack("plant-it", "abandoned", true)
|
|
deployed.State = stacks.StateRunning
|
|
html := renderBackupPage(t, "stacks", map[string]interface{}{
|
|
"Page": "stacks", "Title": "Alkalmazások",
|
|
"Stacks": []stacks.Stack{deployed},
|
|
"MissingStorage": map[string]string{},
|
|
"NetworkWarnings": map[string]string{},
|
|
"NetworkStubs": map[string]string{},
|
|
"StorageLabels": map[string]string{},
|
|
"Subdomains": map[string]string{},
|
|
})
|
|
if !strings.Contains(html, "Nem karbantartott") {
|
|
t.Fatal("a deployed abandoned app must carry the Nem karbantartott badge on its card")
|
|
}
|
|
if strings.Contains(html, "Telepítés</a>") {
|
|
t.Error("an abandoned app must never offer a Telepítés button")
|
|
}
|
|
}
|
|
|
|
// TestAvailableAppHasNoBadgeAndKeepsInstallButton is the negative half — without it, an impl that
|
|
// badges everything would pass the test above.
|
|
func TestAvailableAppHasNoBadgeAndKeepsInstallButton(t *testing.T) {
|
|
html := renderBackupPage(t, "stacks", map[string]interface{}{
|
|
"Page": "stacks", "Title": "Alkalmazások",
|
|
"Stacks": []stacks.Stack{lcStack("bookstack", "", false)},
|
|
"MissingStorage": map[string]string{},
|
|
"NetworkWarnings": map[string]string{},
|
|
"NetworkStubs": map[string]string{},
|
|
"StorageLabels": map[string]string{},
|
|
"Subdomains": map[string]string{},
|
|
})
|
|
if strings.Contains(html, "Nem karbantartott") {
|
|
t.Error("an available app must not be badged")
|
|
}
|
|
if !strings.Contains(html, "Telepítés</a>") {
|
|
t.Fatal("an available, undeployed app must still offer Telepítés")
|
|
}
|
|
}
|