package notify import ( "strings" "testing" ) // hubAcceptedSeverities is the hub's EXACT severity vocabulary, transcribed from // felhom.eu/hub/internal/api/handler.go — the event-ingest handler's `switch payload.Severity` — // where anything outside this set is silently coerced to "info". // // Of these, only warning/error/critical actually reach an email: // felhom.eu/hub/internal/notify/dispatcher.go, func severityNotifies. // // This set is duplicated here ON PURPOSE. A test asserting only `== "warning"` would keep passing if // the hub's vocabulary changed underneath it; pinning the set means the assertion is about the WIRE // CONTRACT, and a reader can check both named locations rather than take this on trust. var hubAcceptedSeverities = map[string]bool{"info": true, "warning": true, "error": true, "critical": true} // hubSeverityNotifies mirrors felhom.eu/hub/internal/notify/dispatcher.go:severityNotifies. func hubSeverityNotifies(sev string) bool { switch sev { case "warning", "error", "critical": return true } return false } // Group K — Scenario K: the severity actually routes. // // This is the highest-value assertion in the change. Before v0.215.0 the Figyelmeztetés-level disk // alert carried severity "warn", which is NOT in the hub's accepted set, so the hub coerced it to // "info" and severityNotifies dropped it — the alert reached no email on either the customer or the // operator leg. // // Red-proof: restore `return "warn"` in DiskAlertKind.severity() → the exact-match assertion, the // accepted-set assertion AND the routes-to-email assertion all fail. func TestNotifyDiskHealthDegraded_SeverityRoutes(t *testing.T) { n := &Notifier{} var gotSev string n.pushFn = func(eventType, severity, message string, details interface{}) { gotSev = severity } n.NotifyDiskHealthDegraded(DiskAlert{Label: "sdb", Kind: DiskAlertWarn, Attributes: []string{"függőben lévő szektorok"}}) if gotSev != "warning" { t.Errorf("Figyelmeztetés severity = %q, want %q", gotSev, "warning") } if !hubAcceptedSeverities[gotSev] { t.Errorf("severity %q is NOT in the hub's accepted set — the hub coerces it to \"info\"", gotSev) } if !hubSeverityNotifies(gotSev) { t.Errorf("severity %q does not route to email — the alert would reach nobody", gotSev) } // Every Hiba kind must also carry a routing severity. for _, k := range []DiskAlertKind{DiskAlertFailSelfReported, DiskAlertFailSectors, DiskAlertFailTemperature, DiskAlertFailWorsened} { n.NotifyDiskHealthDegraded(DiskAlert{Label: "sdc", Kind: k, Sectors: 352, TemperatureC: 61}) if gotSev != "critical" { t.Errorf("kind %d severity = %q, want critical", k, gotSev) } if !hubAcceptedSeverities[gotSev] || !hubSeverityNotifies(gotSev) { t.Errorf("kind %d severity %q does not route", k, gotSev) } } } // The event type + details payload + the Figyelmeztetés copy (unchanged wording, now delivered). func TestNotifyDiskHealthDegraded_WarnShape(t *testing.T) { n := &Notifier{} var gotType, gotMsg string var gotDetails interface{} n.pushFn = func(eventType, severity, message string, details interface{}) { gotType, gotMsg, gotDetails = eventType, message, details } n.NotifyDiskHealthDegraded(DiskAlert{Label: "sdb (lassú)", Kind: DiskAlertWarn, Attributes: []string{"függőben lévő szektorok"}}) if gotType != "disk_health_degraded" { t.Errorf("event type = %q, want disk_health_degraded", gotType) } if !strings.Contains(gotMsg, "Lemez állapot romlás: sdb (lassú)") { t.Errorf("message missing the required subject phrase: %q", gotMsg) } if !strings.Contains(gotMsg, "függőben lévő szektorok") { t.Errorf("message does not name the triggering attribute: %q", gotMsg) } if d, ok := gotDetails.(DiskHealthDetails); !ok || d.Disk != "sdb (lassú)" || d.Critical { t.Errorf("details = %+v, want DiskHealthDetails{Disk:sdb (lassú), Critical:false}", gotDetails) } } // Each Hiba kind produces its OWN message, and each names the fact the customer needs to act on. // A customer told "the drive overheated" must not be told to arrange a replacement, and vice versa. // // Red-proof: collapse the switch so every Fail kind emits the self-reported sentence → the sector // count and the temperature disappear from their messages and the substring assertions fail. func TestNotifyDiskHealthDegraded_FailShapes(t *testing.T) { n := &Notifier{} var gotMsg string var gotDetails interface{} n.pushFn = func(eventType, severity, message string, details interface{}) { gotMsg, gotDetails = message, details } cases := []struct { name string alert DiskAlert wantSubstrs []string notSubstrs []string }{ { name: "drive self-reports FAILING", alert: DiskAlert{Label: "sdc", Kind: DiskAlertFailSelfReported}, wantSubstrs: []string{"Lemez állapot romlás: sdc", "SMART önellenőrzése hibát jelez"}, }, { name: "reached from sector counters", alert: DiskAlert{Label: "ST3000VX010-2E3166", Kind: DiskAlertFailSectors, Sectors: 352}, wantSubstrs: []string{"Lemez hiba: ST3000VX010-2E3166", "352 olvashatatlan szektor", "meghajtó cseréjéhez"}, notSubstrs: []string{"SMART önellenőrzése"}, // the drive said PASSED — must not claim otherwise }, { name: "reached from heat", alert: DiskAlert{Label: "sdd", Kind: DiskAlertFailTemperature, TemperatureC: 61}, wantSubstrs: []string{"Lemez hiba: sdd", "túlmelegedett (61 °C)", "szellőzését"}, notSubstrs: []string{"olvashatatlan szektor"}, }, { name: "already reported, still worsening", alert: DiskAlert{Label: "sdg", Kind: DiskAlertFailWorsened, Sectors: 352}, wantSubstrs: []string{"Lemez hiba: sdg", "tovább romlott", "352 olvashatatlan szektor"}, }, } for _, c := range cases { n.NotifyDiskHealthDegraded(c.alert) for _, w := range c.wantSubstrs { if !strings.Contains(gotMsg, w) { t.Errorf("%s: message %q missing %q", c.name, gotMsg, w) } } for _, bad := range c.notSubstrs { if strings.Contains(gotMsg, bad) { t.Errorf("%s: message %q must not contain %q", c.name, gotMsg, bad) } } if d, ok := gotDetails.(DiskHealthDetails); !ok || !d.Critical { t.Errorf("%s: details %+v, want Critical:true", c.name, gotDetails) } } } // No customer-facing disk copy may carry an emoji (design-system hard rule) and every shape must // name the disk. Cheap, and it covers shapes added later by someone who did not read the rule. func TestNotifyDiskHealthDegraded_CopyDiscipline(t *testing.T) { n := &Notifier{} var gotMsg string n.pushFn = func(eventType, severity, message string, details interface{}) { gotMsg = message } for _, k := range []DiskAlertKind{DiskAlertWarn, DiskAlertFailSelfReported, DiskAlertFailSectors, DiskAlertFailTemperature, DiskAlertFailWorsened} { n.NotifyDiskHealthDegraded(DiskAlert{Label: "TESTDISK", Kind: k, Sectors: 8, TemperatureC: 61}) if !strings.Contains(gotMsg, "TESTDISK") { t.Errorf("kind %d: message does not name the disk: %q", k, gotMsg) } for _, r := range gotMsg { if r > 0x2100 { // emoji / symbol block — Hungarian accents are all well below this t.Errorf("kind %d: message contains a non-text rune %q: %s", k, r, gotMsg) } } } }