Files
HODA/internal/discovery/discovery_test.go
T
TMCandClaude Opus 5.5 9d6a921c34
CI / build (push) Successful in 30s
Start HODA: a homelab dashboard built from Docker labels
Apps opt in with sema.dashboard.* labels in their own compose file. The
primary reads its host's containers through a read-only socket proxy,
follows Docker events, and merges in what the agent on the second host
reports. Icons come from the icon label, the dashboard-icons set or the
app's favicon, with a letter as the fallback.

Go with only the standard library, a distroless image, and a Gitea
Actions workflow copied from FiMa's: checks in the Dockerfile's check
stage, a Trivy scan, and a push to git.mariosem.es/msemes/hoda.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:18:20 +01:00

78 lines
2.2 KiB
Go

package discovery
import (
"context"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"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
}
fmt.Fprint(w, `[
{"Id":"a","Names":["/gitea"],"State":"running","Status":"Up","Labels":{"sema.dashboard.enable":"true","sema.dashboard.url":"https://git.mariosem.es"}},
{"Id":"b","Names":["/broken"],"State":"running","Status":"Up","Labels":{"sema.dashboard.enable":"true"}},
{"Id":"c","Names":["/off"],"State":"running","Status":"Up","Labels":{"sema.dashboard.enable":"false"}}
]`)
}))
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())
hosts, list := store.Snapshot(nil)
if len(list) != 1 || list[0].Name != "gitea" || list[0].Host != "host-1" {
t.Fatalf("apps = %+v, want only gitea", list)
}
if len(icons.got) != 1 {
t.Error("icons were not queued for the apps")
}
if _, warned := d.warned["broken"]; !warned {
t.Error("the container without a url label was not reported")
}
if !hosts[0].OK {
t.Error("host not marked reachable")
}
up = false
d.sync(context.Background())
hosts, list = store.Snapshot(nil)
if hosts[0].OK || len(list) != 1 {
t.Errorf("Docker down: hosts %+v, %d apps; want unreachable with apps kept", hosts, len(list))
}
}
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)
}
}
}