package logpolicy import ( "strings" "testing" "time" ) func TestSanitizeRedactsSecretsAndBoundsUnicode(t *testing.T) { value, redacted := Sanitize("config auth.radius.key radius-secret configure password=hunter2\r\nAuthorization: Bearer abc.def "+strings.Repeat("가", 20), 70) if !redacted { t.Fatal("Sanitize() redacted = false, want true") } if strings.Contains(value, "radius-secret") || strings.Contains(value, "hunter2") || strings.Contains(value, "abc.def") || strings.ContainsAny(value, "\r\n") { t.Fatalf("Sanitize() leaked unsafe text: %q", value) } if !strings.Contains(value, "[REDACTED]") || !strings.Contains(value, "[truncated]") { t.Fatalf("Sanitize() = %q, want redaction and truncation markers", value) } } func TestObservedDefaultStatusCadences(t *testing.T) { if DefaultLinkPollInterval != time.Minute { t.Fatalf("DefaultLinkPollInterval = %s, want 1m", DefaultLinkPollInterval) } if DefaultKeepaliveInterval != 3*time.Minute { t.Fatalf("DefaultKeepaliveInterval = %s, want 3m", DefaultKeepaliveInterval) } if DefaultSnapshotInterval != 30*time.Minute { t.Fatalf("DefaultSnapshotInterval = %s, want 30m", DefaultSnapshotInterval) } tracker := NewLinkTracker(0) start := time.Date(2026, 7, 29, 0, 0, 0, 0, time.UTC) first, err := tracker.Observe(LinkObservation{Kind: "ethernet", Resource: "Interface-1", State: "down", Signal: "carrier", ObservedAt: start}) if err != nil || first == nil || first.EventType != "snapshot" || first.AlarmTriggered { t.Fatalf("first observation = %+v, err=%v", first, err) } quiet, err := tracker.Observe(LinkObservation{Kind: "ethernet", Resource: "Interface-1", State: "down", Signal: "carrier", ObservedAt: start.Add(29 * time.Minute)}) if err != nil || quiet != nil { t.Fatalf("quiet observation = %+v, err=%v", quiet, err) } periodic, err := tracker.Observe(LinkObservation{Kind: "ethernet", Resource: "Interface-1", State: "down", Signal: "carrier", ObservedAt: start.Add(30 * time.Minute)}) if err != nil || periodic == nil || periodic.EventType != "snapshot" || periodic.AlarmTriggered { t.Fatalf("periodic observation = %+v, err=%v", periodic, err) } } func TestMatchCommandUsesCommandOnlyAndStableRuleIDs(t *testing.T) { matches := MatchCommand("show LOGIN FAILED counters", []KeywordRule{ {RuleID: "z_rule", Keyword: "login failed"}, {RuleID: "a_rule", Keyword: "SHOW"}, {RuleID: "a_rule", Keyword: "counters"}, }) if strings.Join(matches, ",") != "a_rule,z_rule" { t.Fatalf("MatchCommand() = %v", matches) } } func TestLinkTrackerSnapshotsTransitionsAndAlarms(t *testing.T) { tracker := NewLinkTracker(5 * time.Minute) start := time.Date(2026, 7, 28, 0, 0, 0, 0, time.UTC) first, err := tracker.Observe(LinkObservation{Kind: "serial", Resource: "CH01", State: "down", Signal: "dcd", ObservedAt: start}) if err != nil || first == nil || first.EventType != "snapshot" || first.AlarmTriggered { t.Fatalf("first observation = %+v, err=%v", first, err) } quiet, err := tracker.Observe(LinkObservation{Kind: "serial", Resource: "CH01", State: "down", Signal: "dcd", ObservedAt: start.Add(time.Minute)}) if err != nil || quiet != nil { t.Fatalf("quiet observation = %+v, err=%v", quiet, err) } up, err := tracker.Observe(LinkObservation{Kind: "serial", Resource: "CH01", State: "up", Signal: "dcd", ObservedAt: start.Add(2 * time.Minute)}) if err != nil || up == nil || up.EventType != "transition" || up.AlarmTriggered { t.Fatalf("up observation = %+v, err=%v", up, err) } down, err := tracker.Observe(LinkObservation{Kind: "serial", Resource: "CH01", State: "down", Signal: "dcd", ObservedAt: start.Add(3 * time.Minute)}) if err != nil || down == nil || !down.AlarmTriggered || down.PreviousState != "up" { t.Fatalf("down observation = %+v, err=%v", down, err) } periodic, err := tracker.Observe(LinkObservation{Kind: "serial", Resource: "CH01", State: "down", Signal: "dcd", ObservedAt: start.Add(9 * time.Minute)}) if err != nil || periodic == nil || periodic.EventType != "snapshot" || periodic.AlarmTriggered { t.Fatalf("periodic observation = %+v, err=%v", periodic, err) } } func TestLinkTrackerRejectsUnverifiablePhysicalState(t *testing.T) { tracker := NewLinkTracker(time.Minute) now := time.Date(2026, 7, 28, 0, 0, 0, 0, time.UTC) for _, observation := range []LinkObservation{ {Kind: "serial", Resource: "CH99", State: "up", Signal: "dcd", ObservedAt: now}, {Kind: "serial", Resource: "CH01", State: "up", Signal: "unknown", ObservedAt: now}, {Kind: "ethernet", Resource: "Interface-1", State: "up", Signal: "dcd", ObservedAt: now}, } { if event, err := tracker.Observe(observation); err == nil || event != nil { t.Fatalf("Observe(%+v) = %+v, %v; want rejection", observation, event, err) } } } func TestLinkTrackerAcceptsUnifiedSerialSignals(t *testing.T) { tracker := NewLinkTracker(time.Minute) now := time.Date(2026, 8, 22, 0, 0, 0, 0, time.UTC) for _, signal := range []string{"cts", "cr"} { event, err := tracker.Observe(LinkObservation{ Kind: "serial", Resource: "CH06", State: "up", Signal: signal, ObservedAt: now, }) if err != nil || event == nil || event.Signal != signal { t.Fatalf("%s observation = %+v, %v", signal, event, err) } } } func TestLinkTrackerCanDisableSnapshotsWithoutLosingTransitions(t *testing.T) { tracker := NewLinkTracker(time.Minute) tracker.SetSnapshotsEnabled(false) start := time.Date(2026, 7, 29, 0, 0, 0, 0, time.UTC) first, err := tracker.Observe(LinkObservation{Kind: "ethernet", Resource: "Interface-1", State: "up", Signal: "carrier", ObservedAt: start}) if err != nil || first != nil { t.Fatalf("disabled first snapshot = %+v, %v", first, err) } repeated, err := tracker.Observe(LinkObservation{Kind: "ethernet", Resource: "Interface-1", State: "up", Signal: "carrier", ObservedAt: start.Add(5 * time.Minute)}) if err != nil || repeated != nil { t.Fatalf("disabled periodic snapshot = %+v, %v", repeated, err) } transition, err := tracker.Observe(LinkObservation{Kind: "ethernet", Resource: "Interface-1", State: "down", Signal: "carrier", ObservedAt: start.Add(6 * time.Minute)}) if err != nil || transition == nil || transition.EventType != "transition" || !transition.AlarmTriggered { t.Fatalf("transition = %+v, %v", transition, err) } }