package unraid import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/itworx/pulse/internal/container" ) func TestContainerSourceAcceptsBoundedOperationalHeadroomAndRejectsOverflow(t *testing.T) { for _, test := range []struct { name string count int wantErr bool }{ {name: "bounded headroom", count: container.DefaultMaxContainers}, {name: "overflow", count: container.DefaultMaxContainers + 1, wantErr: true}, } { t.Run(test.name, func(t *testing.T) { items := make([]map[string]any, 0, test.count) for index := 0; index < test.count; index++ { items = append(items, map[string]any{"id": fmt.Sprintf("id-%03d", index), "names": []string{fmt.Sprintf("/container-%03d", index)}, "state": "RUNNING", "status": "Up 1 hour", "autoStart": true}) } server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _ = json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"docker": map[string]any{"containers": items}}}) })) defer server.Close() client, err := New(server.URL, "test-key", server.Client()) if err != nil { t.Fatal(err) } snapshot, err := (ContainerSource{Client: client}).Snapshot(context.Background()) if test.wantErr { if err == nil { t.Fatal("expected oversized response rejection") } return } if err != nil || len(snapshot.Containers) != test.count { t.Fatalf("containers=%d err=%v", len(snapshot.Containers), err) } }) } } func TestContainerSourceNormalizesDocumentedReadOnlyContainerFields(t *testing.T) { server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"data":{"docker":{"containers":[{"id":"two","names":["/zeta"],"state":"EXITED","status":"Exited (0) 2 days ago","autoStart":false},{"id":"one","names":"/alpha","state":"RUNNING","status":"Up 34 hours (healthy)","autoStart":true},{"id":"three","names":"/beta","state":"RUNNING","status":"Up 34 hours","autoStart":true},{"id":"four","names":"/gamma","state":"EXITED","status":"Restarting (137) 11 seconds ago","autoStart":false}]}}}`)) })) defer server.Close() client, err := New(server.URL, "test-key", server.Client()) if err != nil { t.Fatal(err) } now := time.Date(2026, 8, 10, 2, 0, 0, 0, time.UTC) snapshot, err := (ContainerSource{Client: client, Now: func() time.Time { return now }}).Snapshot(context.Background()) if err != nil { t.Fatal(err) } if len(snapshot.Containers) != 4 || snapshot.Containers[0].Name != "zeta" || !snapshot.Containers[0].IntentionalStop { t.Fatalf("unexpected normalized snapshot: %+v", snapshot) } if snapshot.Containers[0].State != "exited" || snapshot.Containers[0].Health != "unknown" || snapshot.Containers[1].Health != "healthy" || snapshot.Containers[2].Health != "unknown" || snapshot.Containers[3].State != "restarting" || snapshot.Containers[3].IntentionalStop { t.Fatalf("runtime status was mistaken for health: %+v", snapshot.Containers) } if !snapshot.ObservedAt.Equal(now) || snapshot.Source.Type != "unraid" { t.Fatalf("unexpected source timestamps: %+v", snapshot) } } func TestContainerSourceRejectsOversizedOrInvalidPayload(t *testing.T) { server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"data":{"docker":{"containers":[{"id":"","names":[],"state":"running","status":"healthy","autoStart":true}]}}}`)) })) defer server.Close() client, err := New(server.URL, "test-key", server.Client()) if err != nil { t.Fatal(err) } if _, err := (ContainerSource{Client: client}).Snapshot(context.Background()); err == nil { t.Fatal("expected invalid identity rejection") } } func TestContainerStatusClassification(t *testing.T) { healthCases := map[string]string{ "healthy": "healthy", "Up 34 hours (healthy)": "healthy", "unhealthy": "unhealthy", "Up 2 minutes (unhealthy)": "unhealthy", "starting": "starting", "Up 3 seconds (health: starting)": "starting", "Up 34 hours": "unknown", "Exited (0) 2 days ago": "unknown", } for input, want := range healthCases { if got := containerHealth(input); got != want { t.Fatalf("health %q = %q, want %q", input, got, want) } } if got := containerRuntimeState("EXITED", "Restarting (137) 11 seconds ago"); got != "restarting" { t.Fatalf("runtime = %q, want restarting", got) } }