package beach import ( "context" "encoding/binary" "net/http" "net/http/httptest" "strings" "testing" ) func TestParseContainers(t *testing.T) { body := `[ {"Id":"b","Names":["/zeta"],"Image":"nginx","State":"exited","Status":"Exited (0) 2 days ago","Created":1}, {"Id":"a","Names":["/alpha"],"Image":"redis","State":"running","Status":"Up 3 hours (healthy)","Created":2, "Ports":[{"PrivatePort":6379,"PublicPort":6380,"Type":"tcp"},{"PrivatePort":6379,"PublicPort":6380,"Type":"tcp"},{"PrivatePort":9,"Type":"tcp"}]}]` cs, err := parseContainers([]byte(body)) if err != nil { t.Fatal(err) } if len(cs) != 2 || cs[0].Name != "alpha" { t.Fatalf("running containers should sort first: %+v", cs) } if len(cs[0].Ports) != 1 || cs[0].Ports[0] != "6380:6379/tcp" { t.Fatalf("ports not deduplicated or unpublished port kept: %v", cs[0].Ports) } if cs[1].Ports == nil { t.Fatal("ports must be an empty array, not null") } } func frame(stream byte, s string) []byte { h := make([]byte, 8) h[0] = stream binary.BigEndian.PutUint32(h[4:], uint32(len(s))) return append(h, s...) } func TestDemux(t *testing.T) { in := append(frame(1, "hello\n"), frame(2, "oops\n")...) if got := demux(in); got != "hello\noops\n" { t.Fatalf("got %q", got) } if got := demux([]byte("2026-01-01T00:00:00Z plain tty line\n")); !strings.Contains(got, "plain tty line") { t.Fatalf("unframed text lost: %q", got) } } func TestDockerAPI(t *testing.T) { var calls []string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { calls = append(calls, r.Method+" "+r.URL.Path) switch { case r.URL.Path == "/containers/json": w.Write([]byte(`[{"Id":"abc","Names":["/web"],"State":"running","Status":"Up"}]`)) case strings.HasSuffix(r.URL.Path, "/json"): w.Write([]byte(`{"Config":{"Tty":false}}`)) case strings.HasSuffix(r.URL.Path, "/logs"): w.Write(frame(1, "line one\n")) case strings.HasSuffix(r.URL.Path, "/stop"): w.WriteHeader(304) case strings.HasSuffix(r.URL.Path, "/start"): w.WriteHeader(404) w.Write([]byte(`{"message":"No such container: abc"}`)) } })) defer srv.Close() d := NewDocker("tcp://" + strings.TrimPrefix(srv.URL, "http://")) ctx := context.Background() cs, err := d.List(ctx) if err != nil || len(cs) != 1 || cs[0].Name != "web" { t.Fatalf("list: %v %+v", err, cs) } if out, err := d.Logs(ctx, "abc", 50); err != nil || out != "line one\n" { t.Fatalf("logs: %q %v", out, err) } if err := d.Action(ctx, "abc", "stop"); err != nil { t.Fatalf("304 means already stopped, not an error: %v", err) } if err := d.Action(ctx, "abc", "start"); err == nil || !strings.Contains(err.Error(), "No such container") { t.Fatalf("engine message should surface: %v", err) } if err := d.Action(ctx, "abc", "rm"); err == nil { t.Fatal("unknown action must be refused") } } func TestTrueNAS(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != "Bearer secret" { w.WriteHeader(401) return } switch r.URL.Path { case "/api/v2.0/pool": w.Write([]byte(`[{"name":"tank","status":"ONLINE","healthy":true,"size":1000,"allocated":250, "scan":{"function":"SCRUB","state":"FINISHED","errors":0}}, {"name":"fast","status":"DEGRADED","healthy":false,"size":0,"allocated":0,"scan":{"function":"SCRUB","state":"SCANNING","percentage":41.2}}]`)) case "/api/v2.0/disk": w.Write([]byte(`[{"name":"sdb","model":"WD","serial":"S2","size":4000,"pool":"tank"},{"name":"sda","model":"WD","serial":"S1","size":4000,"pool":null}]`)) case "/api/v2.0/disk/temperatures": w.Write([]byte(`{"sda":34,"sdb":null}`)) case "/api/v2.0/alert/list": w.Write([]byte(`[{"level":"WARNING","formatted":"Pool degraded","dismissed":false},{"level":"INFO","formatted":"old","dismissed":true}]`)) default: w.WriteHeader(404) } })) defer srv.Close() st := NewTrueNAS(srv.URL, "secret", false).Collect(context.Background()) if st.Error != "" { t.Fatal(st.Error) } if len(st.Pools) != 2 || st.Pools[0].Name != "fast" { t.Fatalf("pools: %+v", st.Pools) } tank := st.Pools[1] if tank.UsedPct != 25 || tank.Scan != "scrub finished, 0 errors" || !tank.Healthy { t.Fatalf("tank: %+v", tank) } if st.Pools[0].Scan != "scrub in progress 41%" { t.Fatalf("scan progress: %q", st.Pools[0].Scan) } if len(st.Disks) != 2 || st.Disks[0].Name != "sda" || st.Disks[0].TempC != 34 || st.Disks[1].TempC != 0 { t.Fatalf("disks: %+v", st.Disks) } if len(st.Alerts) != 1 || st.Alerts[0].Message != "Pool degraded" { t.Fatalf("dismissed alerts must be hidden: %+v", st.Alerts) } bad := NewTrueNAS(srv.URL, "wrong", false).Collect(context.Background()) if !strings.Contains(bad.Error, "API key rejected") { t.Fatalf("bad key error: %q", bad.Error) } } const smartJSON = `{"device":{"name":"/dev/sda"},"model_name":"ST4000","serial_number":"Z1","user_capacity":{"bytes":4000787030016}, "smart_status":{"passed":%s},"temperature":{"current":38}}` func TestParseSmartctl(t *testing.T) { d, err := parseSmartctl([]byte(strings.Replace(smartJSON, "%s", "true", 1))) if err != nil || d.Health != "PASSED" || d.TempC != 38 || d.Model != "ST4000" { t.Fatalf("%+v %v", d, err) } d, _ = parseSmartctl([]byte(strings.Replace(smartJSON, "%s", "false", 1))) if d.Health != "FAILED" { t.Fatalf("failed disk reported as %q", d.Health) } d, _ = parseSmartctl([]byte(`{"device":{"name":"/dev/sdc"}}`)) if d.Health != "" { t.Fatalf("no verdict must stay unknown, got %q", d.Health) } } func TestLocalSmartAndMerge(t *testing.T) { fake := func(ctx context.Context, args ...string) ([]byte, error) { if args[0] == "--scan" { return []byte(`{"devices":[{"name":"/dev/sda"},{"name":"/dev/sdz"}]}`), nil } if args[len(args)-1] == "/dev/sda" { return []byte(strings.Replace(smartJSON, "%s", "true", 1)), nil } return []byte(`{"device":{"name":"/dev/sdz"},"serial_number":"ONLY","smart_status":{"passed":false}}`), nil } local, err := LocalSmart(context.Background(), fake) if err != nil || len(local) != 2 { t.Fatalf("%v %+v", err, local) } tn := []Disk{{Name: "sda", Serial: "Z1", Source: "truenas"}} merged := mergeDisks(tn, local) if len(merged) != 2 || merged[0].Health != "PASSED" || merged[0].TempC != 38 || merged[0].Source != "truenas" { t.Fatalf("same disk should be merged, not duplicated: %+v", merged) } if merged[1].Serial != "ONLY" || merged[1].Health != "FAILED" { t.Fatalf("smartctl-only disk missing: %+v", merged) } } func TestHostParsers(t *testing.T) { a, err := parseCPU("cpu 100 0 100 700 100 0 0 0 0 0\ncpu0 1 1 1 1 1 1 1 1 1 1\n") if err != nil { t.Fatal(err) } b, _ := parseCPU("cpu 200 0 200 750 150 0 0 0 0 0\n") // between samples: busy +200, idle(+iowait) +100 if got := cpuPct(a, b); got < 66 || got > 67 { t.Fatalf("cpu %.1f", got) } if cpuPct(b, a) != 0 { t.Fatal("a counter reset must not give a negative or huge value") } total, used, st, su := parseMeminfo("MemTotal: 1000 kB\nMemAvailable: 400 kB\nSwapTotal: 200 kB\nSwapFree: 150 kB\n") if total != 1000*1024 || used != 600*1024 || st != 200*1024 || su != 50*1024 { t.Fatalf("%d %d %d %d", total, used, st, su) } if l := parseLoad("0.50 1.25 2.00 1/200 999"); l != [3]float64{0.5, 1.25, 2} { t.Fatalf("%v", l) } if parseUptime("3600.55 7000.1") != 3600.55 { t.Fatal("uptime") } mounts := "/dev/sda1 / ext4 rw 0 0\ntmpfs /run tmpfs rw 0 0\ntank/data /mnt/tank zfs rw 0 0\n/dev/sda1 /var/lib/docker/x ext4 rw 0 0\n/dev/sdb1 /var/lib/docker/y ext4 rw 0 0\n" if got := realMounts(mounts); len(got) != 2 || got[0] != "/" || got[1] != "/mnt/tank" { t.Fatalf("%v", got) } if h := CollectHost(); h.CPUs < 1 { t.Fatal("CollectHost should report CPU count") } } func TestParseTunnels(t *testing.T) { body := `{"sites":[{"id":"s1","name":"homelab","version":"v0.1.8"},{"id":"s2","name":"vps","local":true}], "online":{"s1":true,"s2":false}, "mappings":[{"name":"wiki","proto":"web","hostname":"wiki.example.com","port":443,"enabled":true,"statusKnown":true, "targets":[{},{}],"status":{"targets":[{"healthy":true},{"healthy":false}]}}, {"name":"ssh","proto":"tcp","port":22,"enabled":true,"statusKnown":true,"status":{"error":"bind: in use"}}, {"name":"off","proto":"tcp","port":1,"enabled":false}]}` tn, err := parseTunnels([]byte(body)) if err != nil { t.Fatal(err) } if len(tn.Sites) != 2 || tn.Sites[0].Name != "homelab" || !tn.Sites[0].Online || tn.Sites[1].Online { t.Fatalf("sites: %+v", tn.Sites) } byName := map[string]TunnelPort{} for _, p := range tn.Ports { byName[p.Name] = p } if w := byName["wiki"]; !w.Up || w.Healthy != 1 || w.Targets != 2 { t.Fatalf("wiki: %+v", w) } if byName["ssh"].Up || byName["off"].Up { t.Fatal("errored or disabled ports must not be up") } } func TestFetchTunnels(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/orgs/o1/state" || r.Header.Get("Authorization") != "Bearer k" { w.WriteHeader(401) return } w.Write([]byte(`{"sites":[],"mappings":[],"online":{}}`)) })) defer srv.Close() if tn := FetchTunnels(context.Background(), srv.URL+"/", "k", "o1"); tn.Error != "" { t.Fatal(tn.Error) } if tn := FetchTunnels(context.Background(), srv.URL, "nope", "o1"); !strings.Contains(tn.Error, "401") { t.Fatalf("expected 401 error, got %q", tn.Error) } }