// Copyright 2020-2026 Project Capsule Authors // SPDX-License-Identifier: Apache-2.0 package api import ( "errors" "fmt" "regexp" "testing" ) type fakeExpressionRegexMatcher struct { t *testing.T calls int err error matches map[string]bool seen []ExpressionRegex } func (m *fakeExpressionRegexMatcher) MatchRegex(expr ExpressionRegex, value string) (bool, error) { m.t.Helper() m.calls++ m.seen = append(m.seen, expr) if m.err != nil { return false, m.err } key := fmt.Sprintf("%s|%t|%s", expr.Expression, expr.Negate, value) if matched, ok := m.matches[key]; ok { return matched, nil } re, err := regexp.Compile(expr.Expression) if err != nil { return false, err } matched := re.MatchString(value) if expr.Negate { return !matched, nil } return matched, nil } func TestExpressionMatch_Matches(t *testing.T) { t.Parallel() tests := []struct { name string match ExpressionMatch value string wantMatch bool wantErr bool }{ { name: "exact matches single value", match: ExpressionMatch{ Exact: []string{"default-scheduler"}, }, value: "default-scheduler", wantMatch: true, }, { name: "exact does not match different value", match: ExpressionMatch{ Exact: []string{"default-scheduler"}, }, value: "custom-scheduler", wantMatch: false, }, { name: "exact matches one of multiple values", match: ExpressionMatch{ Exact: []string{"default-scheduler", "custom-scheduler", "team-scheduler"}, }, value: "custom-scheduler", wantMatch: true, }, { name: "exact does not match any of multiple values", match: ExpressionMatch{ Exact: []string{"default-scheduler", "custom-scheduler"}, }, value: "other-scheduler", wantMatch: false, }, { name: "exact is case sensitive", match: ExpressionMatch{ Exact: []string{"Default-Scheduler"}, }, value: "default-scheduler", wantMatch: false, }, { name: "exact uses literal string not pattern", match: ExpressionMatch{ Exact: []string{"team-.*"}, }, value: "team-a", wantMatch: false, }, { name: "exact matches literal pattern string", match: ExpressionMatch{ Exact: []string{"team-.*"}, }, value: "team-.*", wantMatch: true, }, { name: "exact with empty value can match empty string when present", match: ExpressionMatch{ Exact: []string{""}, }, value: "", wantMatch: true, }, { name: "regex matches value", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^team-[a-z0-9-]+$", }, }, value: "team-alpha-1", wantMatch: true, }, { name: "regex does not match value", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^team-[a-z0-9-]+$", }, }, value: "kube-scheduler", wantMatch: false, }, { name: "regex is not implicitly anchored", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "team", }, }, value: "my-team-scheduler", wantMatch: true, }, { name: "invalid regex returns error", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "[", }, }, value: "team-alpha", wantErr: true, }, { name: "combined exact and regex matches by exact", match: ExpressionMatch{ Exact: []string{"default-scheduler"}, ExpressionRegex: ExpressionRegex{ Expression: "^team-[a-z0-9-]+$", }, }, value: "default-scheduler", wantMatch: true, }, { name: "combined exact and regex matches by regex", match: ExpressionMatch{ Exact: []string{"default-scheduler"}, ExpressionRegex: ExpressionRegex{ Expression: "^team-[a-z0-9-]+$", }, }, value: "team-alpha", wantMatch: true, }, { name: "combined exact and regex does not match either", match: ExpressionMatch{ Exact: []string{"default-scheduler"}, ExpressionRegex: ExpressionRegex{ Expression: "^team-[a-z0-9-]+$", }, }, value: "other-scheduler", wantMatch: false, }, { name: "combined exact match skips invalid regex", match: ExpressionMatch{ Exact: []string{"default-scheduler"}, ExpressionRegex: ExpressionRegex{ Expression: "[", }, }, value: "default-scheduler", wantMatch: true, }, { name: "combined exact miss evaluates invalid regex and returns error", match: ExpressionMatch{ Exact: []string{"default-scheduler"}, ExpressionRegex: ExpressionRegex{ Expression: "[", }, }, value: "team-alpha", wantErr: true, }, { name: "negated exact matching value returns false", match: ExpressionMatch{ Exact: []string{"default-scheduler"}, ExpressionRegex: ExpressionRegex{ Negate: true, }, }, value: "default-scheduler", wantMatch: false, }, { name: "negated exact non matching value returns true", match: ExpressionMatch{ Exact: []string{"default-scheduler"}, ExpressionRegex: ExpressionRegex{ Negate: true, }, }, value: "custom-scheduler", wantMatch: true, }, { name: "negated exact with multiple values matching one returns false", match: ExpressionMatch{ Exact: []string{"default-scheduler", "custom-scheduler"}, ExpressionRegex: ExpressionRegex{ Negate: true, }, }, value: "custom-scheduler", wantMatch: false, }, { name: "negated regex matching value returns false", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^trusted/.*", Negate: true, }, }, value: "trusted/platform/app:1", wantMatch: false, }, { name: "negated regex non matching value returns true", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^trusted/.*", Negate: true, }, }, value: "docker.io/library/nginx:latest", wantMatch: true, }, { name: "negated combined exact match returns false", match: ExpressionMatch{ Exact: []string{"trusted/platform/app:1"}, ExpressionRegex: ExpressionRegex{ Expression: "^trusted/.+", Negate: true, }, }, value: "trusted/platform/app:1", wantMatch: false, }, { name: "negated combined regex match returns false", match: ExpressionMatch{ Exact: []string{"trusted/platform/app:1"}, ExpressionRegex: ExpressionRegex{ Expression: "^trusted/.+", Negate: true, }, }, value: "trusted/other/app:1", wantMatch: false, }, { name: "negated combined no match returns true", match: ExpressionMatch{ Exact: []string{"trusted/platform/app:1"}, ExpressionRegex: ExpressionRegex{ Expression: "^trusted/.+", Negate: true, }, }, value: "harbor/platform/app:1", wantMatch: true, }, { name: "empty matcher returns error", match: ExpressionMatch{}, value: "anything", wantErr: true, wantMatch: false, }, { name: "empty exact slice with empty regex returns error", match: ExpressionMatch{ Exact: []string{}, }, value: "anything", wantErr: true, }, { name: "nil exact with whitespace regex is treated as regex and does not match", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: " ", }, }, value: "anything", wantMatch: false, }, { name: "duplicate exact values still match", match: ExpressionMatch{ Exact: []string{"a", "a", "b"}, }, value: "a", wantMatch: true, }, { name: "exact values are not trimmed", match: ExpressionMatch{ Exact: []string{" value "}, }, value: "value", wantMatch: false, }, { name: "exact values match with spaces when value has spaces", match: ExpressionMatch{ Exact: []string{" value "}, }, value: " value ", wantMatch: true, }, { name: "regex can match empty value", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^$", }, }, value: "", wantMatch: true, }, { name: "negated regex can reject empty value", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^$", Negate: true, }, }, value: "", wantMatch: false, }, { name: "negated regex can match non empty value against empty regex", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^$", Negate: true, }, }, value: "non-empty", wantMatch: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := tt.match.Matches(tt.value) if tt.wantErr { if err == nil { t.Fatalf("Matches() expected error, got nil") } return } if err != nil { t.Fatalf("Matches() unexpected error: %v", err) } if got != tt.wantMatch { t.Fatalf("Matches() = %t, want %t", got, tt.wantMatch) } }) } } func TestExpressionMatch_MatchesWithExpressionMatcher_NilMatcherFallback(t *testing.T) { t.Parallel() tests := []struct { name string match ExpressionMatch value string wantMatch bool wantErr bool }{ { name: "nil matcher exact match", match: ExpressionMatch{ Exact: []string{"default-scheduler"}, }, value: "default-scheduler", wantMatch: true, }, { name: "nil matcher regex match", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^team-.*", }, }, value: "team-a", wantMatch: true, }, { name: "nil matcher negated regex non match returns true", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^trusted/.*", Negate: true, }, }, value: "docker.io/library/nginx:latest", wantMatch: true, }, { name: "nil matcher invalid regex returns error", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "[", }, }, value: "team-a", wantErr: true, }, { name: "nil matcher empty expression match returns error", match: ExpressionMatch{}, value: "team-a", wantErr: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := tt.match.MatchesWithExpressionMatcher(nil, tt.value) if tt.wantErr { if err == nil { t.Fatalf("MatchesWithExpressionMatcher(nil) expected error, got nil") } return } if err != nil { t.Fatalf("MatchesWithExpressionMatcher(nil) unexpected error: %v", err) } if got != tt.wantMatch { t.Fatalf("MatchesWithExpressionMatcher(nil) = %t, want %t", got, tt.wantMatch) } }) } } func TestExpressionMatch_MatchesWithExpressionMatcher_UsesMatcherForRegex(t *testing.T) { t.Parallel() matcher := &fakeExpressionRegexMatcher{ t: t, matches: map[string]bool{ "^team-.*|false|team-a": true, }, } match := ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^team-.*", }, } got, err := match.MatchesWithExpressionMatcher(matcher, "team-a") if err != nil { t.Fatalf("MatchesWithExpressionMatcher() unexpected error: %v", err) } if !got { t.Fatalf("MatchesWithExpressionMatcher() = false, want true") } if matcher.calls != 1 { t.Fatalf("MatchRegex() calls = %d, want 1", matcher.calls) } if len(matcher.seen) != 1 { t.Fatalf("seen expressions = %d, want 1", len(matcher.seen)) } if matcher.seen[0].Expression != "^team-.*" { t.Fatalf("seen expression = %q, want %q", matcher.seen[0].Expression, "^team-.*") } if matcher.seen[0].Negate { t.Fatalf("seen negate = true, want false") } } func TestExpressionMatch_MatchesWithExpressionMatcher_PassesNegateToMatcher(t *testing.T) { t.Parallel() matcher := &fakeExpressionRegexMatcher{ t: t, matches: map[string]bool{ "^trusted/.*|true|docker.io/library/nginx:latest": true, }, } match := ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^trusted/.*", Negate: true, }, } got, err := match.MatchesWithExpressionMatcher(matcher, "docker.io/library/nginx:latest") if err != nil { t.Fatalf("MatchesWithExpressionMatcher() unexpected error: %v", err) } if !got { t.Fatalf("MatchesWithExpressionMatcher() = false, want true") } if matcher.calls != 1 { t.Fatalf("MatchRegex() calls = %d, want 1", matcher.calls) } if len(matcher.seen) != 1 { t.Fatalf("seen expressions = %d, want 1", len(matcher.seen)) } if !matcher.seen[0].Negate { t.Fatalf("seen negate = false, want true") } } func TestExpressionMatch_MatchesWithExpressionMatcher_DoesNotUseMatcherWhenExactMatches(t *testing.T) { t.Parallel() matcher := &fakeExpressionRegexMatcher{ t: t, err: errors.New("matcher should not be called"), } match := ExpressionMatch{ Exact: []string{"default-scheduler"}, ExpressionRegex: ExpressionRegex{ Expression: "[", }, } got, err := match.MatchesWithExpressionMatcher(matcher, "default-scheduler") if err != nil { t.Fatalf("MatchesWithExpressionMatcher() unexpected error: %v", err) } if !got { t.Fatalf("MatchesWithExpressionMatcher() = false, want true") } if matcher.calls != 0 { t.Fatalf("MatchRegex() calls = %d, want 0", matcher.calls) } } func TestExpressionMatch_MatchesWithExpressionMatcher_UsesMatcherWhenExactDoesNotMatch(t *testing.T) { t.Parallel() matcher := &fakeExpressionRegexMatcher{ t: t, matches: map[string]bool{ "^team-.*|false|team-a": true, }, } match := ExpressionMatch{ Exact: []string{"default-scheduler"}, ExpressionRegex: ExpressionRegex{ Expression: "^team-.*", }, } got, err := match.MatchesWithExpressionMatcher(matcher, "team-a") if err != nil { t.Fatalf("MatchesWithExpressionMatcher() unexpected error: %v", err) } if !got { t.Fatalf("MatchesWithExpressionMatcher() = false, want true") } if matcher.calls != 1 { t.Fatalf("MatchRegex() calls = %d, want 1", matcher.calls) } } func TestExpressionMatch_MatchesWithExpressionMatcher_ReturnsMatcherError(t *testing.T) { t.Parallel() wantErr := errors.New("compile failed") matcher := &fakeExpressionRegexMatcher{ t: t, err: wantErr, } match := ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^team-.*", }, } got, err := match.MatchesWithExpressionMatcher(matcher, "team-a") if err == nil { t.Fatalf("MatchesWithExpressionMatcher() expected error, got nil") } if !errors.Is(err, wantErr) { t.Fatalf("MatchesWithExpressionMatcher() error = %v, want %v", err, wantErr) } if got { t.Fatalf("MatchesWithExpressionMatcher() = true, want false on error") } if matcher.calls != 1 { t.Fatalf("MatchRegex() calls = %d, want 1", matcher.calls) } } func TestExpressionMatch_MatchesAndMatchesWithExpressionMatcher_AgreeForNilMatcher(t *testing.T) { t.Parallel() tests := []struct { name string match ExpressionMatch value string }{ { name: "exact only", match: ExpressionMatch{ Exact: []string{"a", "b"}, }, value: "a", }, { name: "regex only", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^a+$", }, }, value: "aaa", }, { name: "combined exact and regex exact wins", match: ExpressionMatch{ Exact: []string{"a"}, ExpressionRegex: ExpressionRegex{ Expression: "^b+$", }, }, value: "a", }, { name: "combined exact and regex regex wins", match: ExpressionMatch{ Exact: []string{"a"}, ExpressionRegex: ExpressionRegex{ Expression: "^b+$", }, }, value: "bbb", }, { name: "negated exact", match: ExpressionMatch{ Exact: []string{"a"}, ExpressionRegex: ExpressionRegex{ Negate: true, }, }, value: "b", }, { name: "negated regex", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "^a+$", Negate: true, }, }, value: "bbb", }, { name: "invalid regex", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Expression: "[", }, }, value: "a", }, { name: "empty matcher", match: ExpressionMatch{}, value: "a", }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() gotMatches, errMatches := tt.match.Matches(tt.value) gotWithMatcher, errWithMatcher := tt.match.MatchesWithExpressionMatcher(nil, tt.value) if (errMatches != nil) != (errWithMatcher != nil) { t.Fatalf( "error mismatch: Matches() err=%v, MatchesWithExpressionMatcher(nil) err=%v", errMatches, errWithMatcher, ) } if gotMatches != gotWithMatcher { t.Fatalf( "result mismatch: Matches()=%t, MatchesWithExpressionMatcher(nil)=%t", gotMatches, gotWithMatcher, ) } }) } } func TestContainsExact(t *testing.T) { t.Parallel() tests := []struct { name string values []string value string want bool }{ { name: "nil values", values: nil, value: "a", want: false, }, { name: "empty values", values: []string{}, value: "a", want: false, }, { name: "contains value", values: []string{"a", "b", "c"}, value: "b", want: true, }, { name: "does not contain value", values: []string{"a", "b", "c"}, value: "d", want: false, }, { name: "case sensitive", values: []string{"A"}, value: "a", want: false, }, { name: "empty string", values: []string{""}, value: "", want: true, }, { name: "whitespace is significant", values: []string{" a "}, value: "a", want: false, }, { name: "whitespace matches exactly", values: []string{" a "}, value: " a ", want: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got := containsExact(tt.values, tt.value) if got != tt.want { t.Fatalf("containsExact(%v, %q) = %t, want %t", tt.values, tt.value, got, tt.want) } }) } } func TestExpressionMatch_applyNegate(t *testing.T) { t.Parallel() tests := []struct { name string match ExpressionMatch matched bool want bool }{ { name: "non negated true", match: ExpressionMatch{}, matched: true, want: true, }, { name: "non negated false", match: ExpressionMatch{}, matched: false, want: false, }, { name: "negated true", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Negate: true, }, }, matched: true, want: false, }, { name: "negated false", match: ExpressionMatch{ ExpressionRegex: ExpressionRegex{ Negate: true, }, }, matched: false, want: true, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() got := tt.match.applyNegate(tt.matched) if got != tt.want { t.Fatalf("applyNegate(%t) = %t, want %t", tt.matched, got, tt.want) } }) } }