Files
HODA/internal/discovery/discovery_test.go
T
TMCandClaude Opus 5.5 60b4c79b0f
CI / build (push) Successful in 40s
Show internal URLs, list label problems, copy a label template
Tiles now show the public URL and, when set, the internal URL as a second
link. Label names ignore case and - versus _. Containers HODA can't fully
use are listed above the tiles with the reason: labels without enable,
a missing url, or an unknown label name with a suggestion (category ->
group). To find labels without enable, discovery reads all containers.

A "Copy labels" button copies every label as a commented template, and
falls back to the copy command where the clipboard API is missing (plain
http on the LAN).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:55:06 +01:00

94 lines
3.1 KiB
Go

package discovery
import (
"context"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.mariosem.es/msemes/hoda/internal/apps"
"git.mariosem.es/msemes/hoda/internal/docker"
)
type recorder struct{ got []apps.App }
func (r *recorder) Ensure(list []apps.App) { r.got = list }
func TestSync(t *testing.T) {
up := true
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !up {
http.Error(w, "down", http.StatusBadGateway)
return
}
if r.URL.Query().Get("filters") != "" {
t.Error("containers are filtered, so labels without enable go unreported")
}
fmt.Fprint(w, `[
{"Id":"a","Names":["/gitea"],"State":"running","Status":"Up","Labels":{"sema.dashboard.enable":"true","sema.dashboard.url":"https://git.mariosem.es","sema.dashboard.category":"Dev"}},
{"Id":"b","Names":["/broken"],"State":"running","Status":"Up","Labels":{"sema.dashboard.enable":"true"}},
{"Id":"c","Names":["/forgot"],"State":"running","Status":"Up","Labels":{"sema.dashboard.url":"https://x.example"}},
{"Id":"d","Names":["/off"],"State":"running","Status":"Up","Labels":{"sema.dashboard.enable":"false"}},
{"Id":"e","Names":["/postgres"],"State":"running","Status":"Up","Labels":{"com.docker.compose.service":"db"}}
]`)
}))
defer srv.Close()
dc, err := docker.New(srv.URL)
if err != nil {
t.Fatal(err)
}
store := apps.NewStore()
icons := &recorder{}
d := New(dc, store, icons, "sema.dashboard", "host-1", time.Minute, slog.New(slog.NewTextHandler(io.Discard, nil)))
d.sync(context.Background())
resp := store.Snapshot(nil)
if len(resp.Apps) != 1 || resp.Apps[0].Name != "gitea" || resp.Apps[0].Host != "host-1" {
t.Fatalf("apps = %+v, want only gitea", resp.Apps)
}
if len(icons.got) != 1 {
t.Error("icons were not queued for the apps")
}
var problems []string
for _, issue := range resp.Issues {
problems = append(problems, issue.Container+": "+issue.Message)
}
got := strings.Join(problems, "\n")
for _, want := range []string{"broken: missing sema.dashboard.url", "forgot: has sema.dashboard labels but no", "gitea: unknown label sema.dashboard.category"} {
if !strings.Contains(got, want) {
t.Errorf("issues:\n%s\nwant one starting %q", got, want)
}
}
if len(resp.Issues) != 3 {
t.Errorf("%d issues, want 3 (nothing for the hidden or unlabeled containers)", len(resp.Issues))
}
if !resp.Hosts[0].OK {
t.Error("host not marked reachable")
}
up = false
d.sync(context.Background())
resp = store.Snapshot(nil)
if resp.Hosts[0].OK || len(resp.Apps) != 1 || len(resp.Issues) != 3 {
t.Errorf("Docker down: hosts %+v, %d apps, %d issues; want unreachable with both kept", resp.Hosts, len(resp.Apps), len(resp.Issues))
}
}
func TestRelevant(t *testing.T) {
for _, action := range []string{"start", "stop", "die", "destroy", "create", "health_status: unhealthy", "pause", "rename"} {
if !relevant(action) {
t.Errorf("%q is ignored", action)
}
}
for _, action := range []string{"exec_create: sh -c curl", "exec_start: sh", "exec_die", "attach", "resize", "top", "archive-path"} {
if relevant(action) {
t.Errorf("%q triggers a re-list", action)
}
}
}