F5 (dashboard): surface 'route unpublished' for unhealthy/restarting deployed apps
Traefik only publishes a route to a healthy container, so an unhealthy deployed app returns 404 at its URL though the container runs — previously shown only as 'Nem egészséges' with no hint the URL is dead. New routeUnpublished() funcmap helper + a distinct indicator on the dashboard and stacks cards (gated on .Deployed). Tests: routeUnpublished across all states, real templateFS parses with the funcmap, and the card guard renders the indicator only for deployed+unhealthy.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
)
|
||||
|
||||
// TestRouteUnpublished asserts F5: routeUnpublished is true exactly for the states where Traefik
|
||||
// withholds the public route (unhealthy / restarting) and false otherwise.
|
||||
func TestRouteUnpublished(t *testing.T) {
|
||||
cases := []struct {
|
||||
state stacks.ContainerState
|
||||
want bool
|
||||
}{
|
||||
{stacks.StateUnhealthy, true},
|
||||
{stacks.StateRestarting, true},
|
||||
{stacks.StateRunning, false},
|
||||
{stacks.StateStarting, false},
|
||||
{stacks.StateDeploying, false},
|
||||
{stacks.StateStopped, false},
|
||||
{stacks.StateExited, false},
|
||||
{stacks.StateNotDeployed, false},
|
||||
{stacks.StatePaused, false},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := routeUnpublished(c.state); got != c.want {
|
||||
t.Errorf("routeUnpublished(%q) = %v, want %v", c.state, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestTemplatesParseWithFuncmap asserts the real embedded templates (including the stacks.html /
|
||||
// dashboard.html edits that reference routeUnpublished) parse with the production funcmap. Catches an
|
||||
// unregistered func or a template syntax error introduced by the F5 edits.
|
||||
func TestTemplatesParseWithFuncmap(t *testing.T) {
|
||||
s := &Server{cfg: &config.Config{}}
|
||||
if _, err := template.New("").Funcs(s.templateFuncMap()).ParseFS(templateFS, "templates/*.html"); err != nil {
|
||||
t.Fatalf("templates failed to parse with funcmap: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRouteUnpublishedIndicatorRenders asserts the dashboard/stacks card guard renders the distinct
|
||||
// indicator for a DEPLOYED + unhealthy stack, and NOT for a healthy one — the exact condition both
|
||||
// edited templates use ({{if and .Deployed (routeUnpublished .State)}}).
|
||||
func TestRouteUnpublishedIndicatorRenders(t *testing.T) {
|
||||
s := &Server{cfg: &config.Config{}}
|
||||
const frag = `{{if and .Deployed (routeUnpublished .State)}}URL-NOT-PUBLISHED{{end}}`
|
||||
tmpl, err := template.New("frag").Funcs(s.templateFuncMap()).Parse(frag)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
type row struct {
|
||||
Deployed bool
|
||||
State stacks.ContainerState
|
||||
}
|
||||
render := func(r row) string {
|
||||
var b bytes.Buffer
|
||||
if err := tmpl.Execute(&b, r); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
if got := render(row{Deployed: true, State: stacks.StateUnhealthy}); !strings.Contains(got, "URL-NOT-PUBLISHED") {
|
||||
t.Errorf("deployed+unhealthy should show the indicator, got %q", got)
|
||||
}
|
||||
if got := render(row{Deployed: true, State: stacks.StateRunning}); strings.Contains(got, "URL-NOT-PUBLISHED") {
|
||||
t.Errorf("deployed+running must NOT show the indicator, got %q", got)
|
||||
}
|
||||
if got := render(row{Deployed: false, State: stacks.StateUnhealthy}); strings.Contains(got, "URL-NOT-PUBLISHED") {
|
||||
t.Errorf("not-deployed must NOT show the indicator, got %q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user