CI / build (push) Successful in 40s
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>
102 lines
3.3 KiB
Go
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)
|
|
}
|
|
}
|