package service import ( "crypto/sha256" "encoding/hex" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "metis/pkg/sentinel" ) func TestUIAuthGuardsState(t *testing.T) { app := newTestApp(t) handler := app.Handler() req := httptest.NewRequest(http.MethodGet, "/api/state", nil) resp := httptest.NewRecorder() handler.ServeHTTP(resp, req) if resp.Code != http.StatusForbidden { t.Fatalf("expected forbidden, got %d", resp.Code) } req = httptest.NewRequest(http.MethodGet, "/api/state", nil) req.Header.Set("X-Auth-Request-User", "brad") req.Header.Set("X-Auth-Request-Groups", "admin") resp = httptest.NewRecorder() handler.ServeHTTP(resp, req) if resp.Code != http.StatusOK { t.Fatalf("expected ok, got %d: %s", resp.Code, resp.Body.String()) } } func TestInternalSnapshotAndWatch(t *testing.T) { app := newTestApp(t) handler := app.Handler() payload := `{"node":"titan-15","collected_at":"2026-03-31T12:00:00Z","snapshot":{"hostname":"titan-15","kernel":"6.6.63","os_image":"Armbian","k3s_version":"v1.31.5+k3s1","containerd":"2.0.0","package_sample":{"containerd":"2.0.0"}}}` req := httptest.NewRequest(http.MethodPost, "/internal/sentinel/snapshot", strings.NewReader(payload)) req.Header.Set("Content-Type", "application/json") resp := httptest.NewRecorder() handler.ServeHTTP(resp, req) if resp.Code != http.StatusOK { t.Fatalf("snapshot failed: %d %s", resp.Code, resp.Body.String()) } req = httptest.NewRequest(http.MethodPost, "/internal/sentinel/watch", nil) resp = httptest.NewRecorder() handler.ServeHTTP(resp, req) if resp.Code != http.StatusOK { t.Fatalf("watch failed: %d %s", resp.Code, resp.Body.String()) } var event Event if err := json.Unmarshal(resp.Body.Bytes(), &event); err != nil { t.Fatalf("decode watch response: %v", err) } if event.Kind != "sentinel.watch" { t.Fatalf("unexpected event kind: %s", event.Kind) } metricsReq := httptest.NewRequest(http.MethodGet, "/metrics", nil) metricsResp := httptest.NewRecorder() handler.ServeHTTP(metricsResp, metricsReq) body := metricsResp.Body.String() if !strings.Contains(body, `metis_sentinel_snapshots_total{node="titan-15",status="ok"} 1`) { t.Fatalf("missing snapshot metric: %s", body) } if !strings.Contains(body, `metis_sentinel_watch_total{status="ok"} 1`) { t.Fatalf("missing watch metric: %s", body) } } func newTestApp(t *testing.T) *App { t.Helper() dir := t.TempDir() baseImage := filepath.Join(dir, "base.img") if err := os.WriteFile(baseImage, []byte("test-image"), 0o644); err != nil { t.Fatal(err) } sum := sha256.Sum256([]byte("test-image")) inventoryPath := filepath.Join(dir, "inventory.yaml") inv := ` classes: - name: rpi4 arch: arm64 os: armbian image: file://` + baseImage + ` checksum: sha256:` + hex.EncodeToString(sum[:]) + ` k3s_version: v1.31.5+k3s1 nodes: - name: titan-15 class: rpi4 hostname: titan-15 ip: 192.168.22.43 k3s_role: agent k3s_url: https://192.168.22.7:6443 k3s_token: token ssh_user: atlas ` if err := os.WriteFile(inventoryPath, []byte(inv), 0o644); err != nil { t.Fatal(err) } settings := Settings{ BindAddr: ":0", InventoryPath: inventoryPath, CacheDir: filepath.Join(dir, "cache"), ArtifactDir: filepath.Join(dir, "artifacts"), HistoryPath: filepath.Join(dir, "history.jsonl"), SnapshotsPath: filepath.Join(dir, "snapshots.json"), TargetsPath: filepath.Join(dir, "targets.json"), DefaultFlashHost: "titan-22", FlashHosts: []string{"titan-22"}, LocalHost: "titan-22", AllowedGroups: []string{"admin", "maintainer"}, MaxDeviceBytes: 300000000000, } app, err := NewApp(settings) if err != nil { t.Fatalf("new app: %v", err) } if err := app.StoreSnapshot(SnapshotRecord{ Node: "titan-17", CollectedAt: time.Now().UTC().Add(-10 * time.Minute), Snapshot: sentinelSnapshot("titan-17", "6.6.63"), }); err != nil { t.Fatalf("seed snapshot: %v", err) } return app } func sentinelSnapshot(hostname, kernel string) sentinel.Snapshot { return sentinel.Snapshot{ Hostname: hostname, Kernel: kernel, OSImage: "Armbian", K3sVersion: "v1.31.5+k3s1", Containerd: "2.0.0", PackageSample: map[string]string{"containerd": "2.0.0"}, } }