diff --git a/cmd/karma/views_test.go b/cmd/karma/views_test.go index 10b198afe..03f820392 100644 --- a/cmd/karma/views_test.go +++ b/cmd/karma/views_test.go @@ -331,6 +331,75 @@ func TestAlerts(t *testing.T) { } } +func TestAlertsFilterValues(t *testing.T) { + // verifies that the Value field in the API response is correctly populated + // for all filter types, including those with custom Value() overrides + type filterValueTest struct { + expression string + expectedValue string + isValid bool + } + tests := []filterValueTest{ + // labelFilter — uses filterBase.Value() + {expression: "alertname=HTTP_Probe_Failed", expectedValue: "HTTP_Probe_Failed", isValid: true}, + // receiverFilter — uses filterBase.Value() + {expression: "@receiver=by-cluster-service", expectedValue: "by-cluster-service", isValid: true}, + // ageFilter — custom Value() returns formatted duration + {expression: "@age>1h", expectedValue: "-1h0m0s", isValid: true}, + // inhibitedFilter — custom Value() returns strconv.FormatBool + {expression: "@inhibited=true", expectedValue: "true", isValid: true}, + // limitFilter — custom Value() returns strconv.Itoa + {expression: "@limit=5", expectedValue: "5", isValid: true}, + // fuzzyFilter — custom Value() returns the compiled regex pattern + {expression: "abc", expectedValue: "(?i)abc", isValid: true}, + // invalid filter — filterBase.Value() returns empty string + {expression: "@state=bogus", expectedValue: "", isValid: false}, + } + + mockConfig(t.Setenv) + for _, version := range mock.ListAllMocks() { + mockAlerts(version) + r := testRouter() + setupRouter(r, nil) + + for _, ft := range tests { + t.Run(fmt.Sprintf("%s/%s", version, ft.expression), func(t *testing.T) { + payload, err := json.Marshal(models.AlertsRequest{ + Filters: []string{ft.expression}, + GridLimits: map[string]int{}, + DefaultGroupLimit: 5, + }) + if err != nil { + t.Fatal(err) + } + + apiCache.Purge() + req := httptest.NewRequest("POST", "/alerts.json", bytes.NewReader(payload)) + resp := httptest.NewRecorder() + r.ServeHTTP(resp, req) + if resp.Code != http.StatusOK { + t.Fatalf("POST /alerts.json returned status %d", resp.Code) + } + + ur := models.AlertsResponse{} + err = json.Unmarshal(resp.Body.Bytes(), &ur) + if err != nil { + t.Fatalf("Failed to unmarshal response: %s", err) + } + if len(ur.Filters) != 1 { + t.Fatalf("expected 1 filter in response, got %d", len(ur.Filters)) + } + if ur.Filters[0].Value != ft.expectedValue { + t.Errorf("filter Value = %q, want %q", ur.Filters[0].Value, ft.expectedValue) + } + if ur.Filters[0].IsValid != ft.isValid { + t.Errorf("filter IsValid = %v, want %v", ur.Filters[0].IsValid, ft.isValid) + } + }) + } + } +} + func TestAlertsBadRequest(t *testing.T) { mockConfig(t.Setenv) for _, version := range mock.ListAllMocks() { diff --git a/internal/filters/filter_age.go b/internal/filters/filter_age.go index 03d7ba62a..15f2a8ef1 100644 --- a/internal/filters/filter_age.go +++ b/internal/filters/filter_age.go @@ -40,10 +40,8 @@ func newAgeFilter(name, operator, rawText, value string) Filter { if dur > 0 { dur = -dur } - m, ok := buildMatcher(operator, value) - if !ok { - return &filterBase{rawText: rawText} - } + // operator is pre-validated by the registry, buildMatcher cannot fail here + m, _ := buildMatcher(operator, value) return &ageFilter{ filterBase: filterBase{ matcher: m, diff --git a/internal/filters/filter_fingerprint.go b/internal/filters/filter_fingerprint.go index 8b8a579aa..99d4bd6d9 100644 --- a/internal/filters/filter_fingerprint.go +++ b/internal/filters/filter_fingerprint.go @@ -26,10 +26,8 @@ func (filter *fingerprintFilter) MatchAlertmanager(am *models.AlertmanagerInstan } func newFingerprintFilter(name, operator, rawText, value string) Filter { - m, ok := buildMatcher(operator, value) - if !ok { - return &filterBase{rawText: rawText} - } + // operator is pre-validated by the registry, buildMatcher cannot fail here + m, _ := buildMatcher(operator, value) return &fingerprintFilter{ filterBase: filterBase{ matcher: m, diff --git a/internal/filters/filter_inhibited.go b/internal/filters/filter_inhibited.go index ef8be075b..3bccbc155 100644 --- a/internal/filters/filter_inhibited.go +++ b/internal/filters/filter_inhibited.go @@ -47,10 +47,8 @@ func newInhibitedFilter(name, operator, rawText, value string) Filter { default: return &filterBase{rawText: rawText} } - m, ok := buildMatcher(operator, value) - if !ok { - return &filterBase{rawText: rawText} - } + // operator is pre-validated by the registry, buildMatcher cannot fail here + m, _ := buildMatcher(operator, value) return &inhibitedFilter{ filterBase: filterBase{ matcher: m, diff --git a/internal/filters/filter_inhibited_by.go b/internal/filters/filter_inhibited_by.go index b897bdc65..6ae9469df 100644 --- a/internal/filters/filter_inhibited_by.go +++ b/internal/filters/filter_inhibited_by.go @@ -35,10 +35,8 @@ func (filter *inhibitedByFilter) MatchAlertmanager(am *models.AlertmanagerInstan } func newInhibitedByFilter(name, operator, rawText, value string) Filter { - m, ok := buildMatcher(operator, value) - if !ok { - return &filterBase{rawText: rawText} - } + // operator is pre-validated by the registry, buildMatcher cannot fail here + m, _ := buildMatcher(operator, value) return &inhibitedByFilter{ filterBase: filterBase{ matcher: m, diff --git a/internal/filters/filter_limit.go b/internal/filters/filter_limit.go index 72b795d07..6a1f8d10b 100644 --- a/internal/filters/filter_limit.go +++ b/internal/filters/filter_limit.go @@ -30,10 +30,8 @@ func newLimitFilter(name, operator, rawText, value string) Filter { if err != nil || val < 1 { return &filterBase{rawText: rawText} } - m, ok := buildMatcher(operator, value) - if !ok { - return &filterBase{rawText: rawText} - } + // operator is pre-validated by the registry, buildMatcher cannot fail here + m, _ := buildMatcher(operator, value) return &limitFilter{ filterBase: filterBase{ matcher: m, diff --git a/internal/filters/filter_silenced_by.go b/internal/filters/filter_silenced_by.go index d1348d03a..22dedf28e 100644 --- a/internal/filters/filter_silenced_by.go +++ b/internal/filters/filter_silenced_by.go @@ -35,10 +35,8 @@ func (filter *silenceIDFilter) MatchAlertmanager(am *models.AlertmanagerInstance } func newSilenceIDFilter(name, operator, rawText, value string) Filter { - m, ok := buildMatcher(operator, value) - if !ok { - return &filterBase{rawText: rawText} - } + // operator is pre-validated by the registry, buildMatcher cannot fail here + m, _ := buildMatcher(operator, value) return &silenceIDFilter{ filterBase: filterBase{ matcher: m, diff --git a/internal/filters/filter_state.go b/internal/filters/filter_state.go index 76edf962a..3435ddb51 100644 --- a/internal/filters/filter_state.go +++ b/internal/filters/filter_state.go @@ -31,10 +31,8 @@ func newStateFilter(name, operator, rawText, value string) Filter { if _, ok := models.AlertStateFromString(value); !ok { return &filterBase{rawText: rawText} } - m, ok := buildMatcher(operator, value) - if !ok { - return &filterBase{rawText: rawText} - } + // operator is pre-validated by the registry, buildMatcher cannot fail here + m, _ := buildMatcher(operator, value) return &stateFilter{ filterBase: filterBase{ matcher: m, diff --git a/internal/filters/filter_test.go b/internal/filters/filter_test.go index 6b084f142..b98d61893 100644 --- a/internal/filters/filter_test.go +++ b/internal/filters/filter_test.go @@ -1028,6 +1028,32 @@ var tests = []filterTest{ }, IsMatch: true, }, + + // invalid regex pattern triggers buildMatcher failure in regex-capable filters + { + Expression: "@alertmanager=~[", + IsValid: false, + }, + { + Expression: "@cluster=~[", + IsValid: false, + }, + { + Expression: "@receiver=~[", + IsValid: false, + }, + { + Expression: "@silence_ticket=~[", + IsValid: false, + }, + { + Expression: "@silence_author=~[", + IsValid: false, + }, + { + Expression: "node=~[", + IsValid: false, + }, } func TestFilters(t *testing.T) {