package web import ( "context" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/agentapi" ) func smartPtr(v int) *int { return &v } func physDisk(name string, sm *agentapi.SmartSummary) agentapi.DiskInfo { return agentapi.DiskInfo{Name: name, BackingDevice: "/dev/" + name, DurableID: "uuid:" + name, Smart: sm} } // diskCheckHarness wires a Server with the disks source + notify sink seams and returns a captured // list of emitted disk labels. func diskCheckHarness(t *testing.T) (*Server, *[]string, *[]agentapi.DiskInfo) { t.Helper() s := testServer(t) var fired []string payload := &[]agentapi.DiskInfo{} s.diskNotifyFn = func(label string, attrs []string, critical bool) { fired = append(fired, label) } s.disksFn = func(ctx context.Context) (agentapi.DisksResponse, error) { return agentapi.DisksResponse{Disks: *payload}, nil } return s, &fired, payload } // Scenario B — first run baselines silently; a real degradation (OK→Warn) emits exactly once; a // steady-state re-check does not re-emit. Red-proof: remove the `firstRun || !had` guard → the first // run emits and the "no notify on first run" assertion fails. func TestDiskHealthCheck_DegradationOnce(t *testing.T) { s, fired, payload := diskCheckHarness(t) ctx := context.Background() // First check: disk PASSED clean (verdict OK). Baseline only. *payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartPassed})} _ = s.RunDiskHealthCheck(ctx) if len(*fired) != 0 { t.Fatalf("first run must not notify, got %v", *fired) } // Degrade: pending sectors 0→5 (OK→Figyelmeztetés). *payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartPassed, PendingSectors: smartPtr(5)})} _ = s.RunDiskHealthCheck(ctx) if len(*fired) != 1 { t.Fatalf("degradation must emit exactly once, got %v", *fired) } // Steady state: still Figyelmeztetés — no repeat. _ = s.RunDiskHealthCheck(ctx) if len(*fired) != 1 { t.Fatalf("steady-state degraded must not re-emit, got %v", *fired) } } // Scenario B (cont.) — recovery (Figyelmeztetés→Rendben) notifies nothing. func TestDiskHealthCheck_RecoverySilent(t *testing.T) { s, fired, payload := diskCheckHarness(t) ctx := context.Background() *payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartPassed})} _ = s.RunDiskHealthCheck(ctx) // baseline OK *payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartPassed, PendingSectors: smartPtr(5)})} _ = s.RunDiskHealthCheck(ctx) // OK→Warn: emits if len(*fired) != 1 { t.Fatalf("expected 1 emit on degradation, got %v", *fired) } // Recover back to clean. *payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartPassed})} _ = s.RunDiskHealthCheck(ctx) if len(*fired) != 1 { t.Errorf("recovery must not notify, got %v", *fired) } } // Scenario C — UNKNOWN is excluded both directions: a UNKNOWN disk never baselines/emits, and an // OK→UNKNOWN→Warn sequence fires on the real OK→Warn (the UNKNOWN blip is ignored, not treated as a // transition). Red-proof: the truth of "excluded both directions" — if UNKNOWN were recorded as a // verdict, UNKNOWN→Warn would look like a degradation from a low baseline. func TestDiskHealthCheck_UnknownExcluded(t *testing.T) { s, fired, payload := diskCheckHarness(t) ctx := context.Background() // A purely-UNKNOWN disk: first run + repeat, never notifies, never records. *payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartUnknown})} _ = s.RunDiskHealthCheck(ctx) _ = s.RunDiskHealthCheck(ctx) if len(*fired) != 0 { t.Fatalf("UNKNOWN disk must never notify, got %v", *fired) } // OK baseline, then a UNKNOWN blip, then Warn — must fire once (OK→Warn), the blip ignored. *payload = []agentapi.DiskInfo{physDisk("sdc", &agentapi.SmartSummary{Health: agentapi.SmartPassed})} _ = s.RunDiskHealthCheck(ctx) // baseline OK *payload = []agentapi.DiskInfo{physDisk("sdc", &agentapi.SmartSummary{Health: agentapi.SmartUnknown})} _ = s.RunDiskHealthCheck(ctx) // UNKNOWN blip: no change, no emit if len(*fired) != 0 { t.Fatalf("UNKNOWN blip must not emit, got %v", *fired) } *payload = []agentapi.DiskInfo{physDisk("sdc", &agentapi.SmartSummary{Health: agentapi.SmartPassed, PendingSectors: smartPtr(3)})} _ = s.RunDiskHealthCheck(ctx) // OK→Warn (the blip was ignored): fire once if len(*fired) != 1 { t.Fatalf("real OK→Warn after a UNKNOWN blip must fire once, got %v", *fired) } } // Scenario C — the card renders gracefully with nil SMART (old agent / no data): the row shows // "Nincs adat" and never errors; a physical disk with no smart field is still listed. func TestDiskHealthRows_NilSmart(t *testing.T) { s, _, payload := diskCheckHarness(t) *payload = []agentapi.DiskInfo{ {Name: "sdb", BackingDevice: "/dev/sdb", Smart: nil}, // physical, no smart → Nincs adat // PBS/LVM carry a default UNKNOWN SMART from the agent — must STILL be excluded (not disks). {Name: "felhom-pbs", Type: "pbs", Smart: &agentapi.SmartSummary{Health: agentapi.SmartUnknown}}, {Name: "local-lvm", Type: "lvmthin", Smart: &agentapi.SmartSummary{Health: agentapi.SmartUnknown}}, {Name: "sdc", BackingDevice: "/dev/sdc", Smart: &agentapi.SmartSummary{Health: agentapi.SmartPassed, TemperatureC: smartPtr(31)}}, // Rendben, 31°C } rows := s.diskHealthRows(context.Background()) if len(rows) != 2 { t.Fatalf("want 2 physical-disk rows (pbs+lvm excluded), got %d: %+v", len(rows), rows) } byLabel := map[string]DiskHealthRow{} for _, r := range rows { byLabel[r.Label] = r } if byLabel["sdb"].ChipLabel != "Nincs adat" { t.Errorf("nil-smart disk chip = %q, want Nincs adat", byLabel["sdb"].ChipLabel) } if byLabel["sdc"].ChipLabel != "Rendben" || byLabel["sdc"].Temp != "31" { t.Errorf("sdc row = %+v, want Rendben / 31", byLabel["sdc"]) } } // A degraded verdict is FAILING → critical (Scenario B, Hiba→critical path). func TestDiskHealthCheck_FailingCritical(t *testing.T) { s := testServer(t) var crit []bool s.diskNotifyFn = func(label string, attrs []string, critical bool) { crit = append(crit, critical) } payload := &[]agentapi.DiskInfo{} s.disksFn = func(ctx context.Context) (agentapi.DisksResponse, error) { return agentapi.DisksResponse{Disks: *payload}, nil } ctx := context.Background() *payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartPassed})} _ = s.RunDiskHealthCheck(ctx) // baseline OK *payload = []agentapi.DiskInfo{physDisk("sdb", &agentapi.SmartSummary{Health: agentapi.SmartFailing})} _ = s.RunDiskHealthCheck(ctx) // OK→Fail if len(crit) != 1 || !crit[0] { t.Fatalf("OK→FAILING must emit one critical event, got %v", crit) } } // TTL cache (Scenario A): two card fetches inside 60s hit the agent once. func TestCachedDisks_TTL(t *testing.T) { s := testServer(t) calls := 0 s.disksFn = func(ctx context.Context) (agentapi.DisksResponse, error) { calls++ return agentapi.DisksResponse{}, nil } ctx := context.Background() _, _ = s.cachedDisks(ctx) _, _ = s.cachedDisks(ctx) if calls != 1 { t.Errorf("two fetches within the TTL should call the agent once, got %d", calls) } } func sptr(s string) *string { return &s } // v0.171.0: the card label prefers the device model (agent v0.95.0) over the raw name/UUID; it falls // back to Name (+ speed hint) on an older agent or a modelless disk. // Red-proof: drop the fallback (always return ModelName) → the nil-model/old-agent cases return "" and // TestDiskDisplayLabel_PrefersModel fails (A4 — old-payload tolerance). func TestDiskDisplayLabel_PrefersModel(t *testing.T) { withModel := agentapi.DiskInfo{Name: "47a3361a-uuid", Class: "slow", Smart: &agentapi.SmartSummary{Health: agentapi.SmartPassed, ModelName: sptr("TOSHIBA MQ04ABF100")}} if got := diskDisplayLabel(withModel); got != "TOSHIBA MQ04ABF100" { t.Errorf("label = %q, want the model name", got) } // A4: an old (v0.94.0) agent carries no model → Name (+ speed hint), byte-identical to before. oldAgent := agentapi.DiskInfo{Name: "local", Class: "fast", Smart: &agentapi.SmartSummary{Health: agentapi.SmartUnknown}} if got := diskDisplayLabel(oldAgent); got != "local (gyors)" { t.Errorf("old-agent label = %q, want 'local (gyors)'", got) } // No SMART at all → bare Name. if got := diskDisplayLabel(agentapi.DiskInfo{Name: "sdb"}); got != "sdb" { t.Errorf("nil-smart label = %q, want 'sdb'", got) } }