Files
HODA/internal/peer/peer_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

102 lines
3.3 KiB
Go

package peer
import (
"context"
"encoding/json"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"git.mariosem.es/msemes/hoda/internal/apps"
)
var (
immichID = apps.ID("host-2", "immich")
quiet = slog.New(slog.NewTextHandler(io.Discard, nil))
)
func agent(t *testing.T, iconHits *atomic.Int32) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Authorization") != "Bearer s3cret" {
http.Error(w, "no", http.StatusUnauthorized)
return
}
switch r.URL.Path {
case "/api/apps":
_ = json.NewEncoder(w).Encode(apps.Response{
Node: "host-2",
Hosts: []apps.HostStatus{{Name: "host-2", OK: true}},
Apps: []apps.App{
{ID: immichID, Name: "Immich", URL: "https://photos.mariosem.es", InternalURL: "javascript:alert(2)", Group: "Media", Host: "host-2", Icon: "/icons/" + immichID + "?v=v1"},
{ID: apps.ID("host-2", "evil"), Name: "Evil", URL: "javascript:alert(1)", Host: "host-2"},
{ID: "../../etc", Name: "Bad ID", URL: "https://x.example", Host: "host-2"},
},
Issues: []apps.Issue{{Container: "jellyfin", Message: "missing sema.dashboard.url, so it isn't shown"}},
})
case "/icons/" + immichID:
iconHits.Add(1)
w.Header().Set("Content-Type", "image/png")
_, _ = w.Write([]byte("png"))
default:
http.NotFound(w, r)
}
}))
t.Cleanup(srv.Close)
return srv
}
func TestPull(t *testing.T) {
var hits atomic.Int32
srv := agent(t, &hits)
store := apps.NewStore()
p := New([]string{srv.URL}, "s3cret", time.Second, store, quiet)
p.pull(context.Background(), srv.URL)
resp := store.Snapshot(nil)
if len(resp.Apps) != 1 || resp.Apps[0].Name != "Immich" {
t.Fatalf("apps = %+v, want only the valid app", resp.Apps)
}
if resp.Apps[0].InternalURL != "" {
t.Errorf("internal URL %q from the agent was kept, want a non-web URL dropped", resp.Apps[0].InternalURL)
}
if len(resp.Hosts) != 1 || !resp.Hosts[0].OK || resp.Hosts[0].Name != "host-2" {
t.Errorf("hosts = %+v", resp.Hosts)
}
if len(resp.Issues) != 1 || resp.Issues[0].Host != "host-2" || resp.Issues[0].Container != "jellyfin" {
t.Errorf("issues = %+v, want the agent's issue under its host", resp.Issues)
}
ic, ok := p.Icon(context.Background(), immichID)
if !ok || string(ic.Data) != "png" || ic.Version != "v1" {
t.Fatalf("Icon = %+v, %v", ic, ok)
}
p.Icon(context.Background(), immichID)
if hits.Load() != 1 {
t.Errorf("icon fetched %d times, want once per version", hits.Load())
}
srv.Close()
p.pull(context.Background(), srv.URL)
resp = store.Snapshot(nil)
if len(resp.Apps) != 1 || resp.Hosts[0].OK || resp.Hosts[0].Name != "host-2" || resp.Hosts[0].LastOK == nil {
t.Errorf("after the agent went away: hosts %+v, %d apps; want it unreachable with its apps kept", resp.Hosts, len(resp.Apps))
}
}
func TestWrongToken(t *testing.T) {
var hits atomic.Int32
srv := agent(t, &hits)
store := apps.NewStore()
p := New([]string{srv.URL}, "wrong", time.Second, store, quiet)
p.pull(context.Background(), srv.URL)
resp := store.Snapshot(nil)
if len(resp.Apps) != 0 || len(resp.Hosts) != 1 || resp.Hosts[0].OK || resp.Hosts[0].Name != "127.0.0.1" {
t.Errorf("hosts %+v, apps %+v; want the agent unreachable under its address", resp.Hosts, resp.Apps)
}
}