snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
This commit is contained in:
273
internal/xds/matcher/matcher_header.go
Normal file
273
internal/xds/matcher/matcher_header.go
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// HeaderMatcher is an interface for header matchers. These are
|
||||
// documented in (EnvoyProxy link here?). These matchers will match on different
|
||||
// aspects of HTTP header name/value pairs.
|
||||
type HeaderMatcher interface {
|
||||
Match(metadata.MD) bool
|
||||
String() string
|
||||
}
|
||||
|
||||
// valueFromMD retrieves metadata from context. If there are
|
||||
// multiple values, the values are concatenated with "," (comma and no space).
|
||||
//
|
||||
// All header matchers only match against the comma-concatenated string.
|
||||
func valueFromMD(md metadata.MD, key string) (string, bool) {
|
||||
vs, ok := md[key]
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return strings.Join(vs, ","), true
|
||||
}
|
||||
|
||||
// HeaderExactMatcher matches on an exact match of the value of the header.
|
||||
type HeaderExactMatcher struct {
|
||||
key string
|
||||
exact string
|
||||
invert bool
|
||||
}
|
||||
|
||||
// NewHeaderExactMatcher returns a new HeaderExactMatcher.
|
||||
func NewHeaderExactMatcher(key, exact string, invert bool) *HeaderExactMatcher {
|
||||
return &HeaderExactMatcher{key: key, exact: exact, invert: invert}
|
||||
}
|
||||
|
||||
// Match returns whether the passed in HTTP Headers match according to the
|
||||
// HeaderExactMatcher.
|
||||
func (hem *HeaderExactMatcher) Match(md metadata.MD) bool {
|
||||
v, ok := valueFromMD(md, hem.key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return (v == hem.exact) != hem.invert
|
||||
}
|
||||
|
||||
func (hem *HeaderExactMatcher) String() string {
|
||||
return fmt.Sprintf("headerExact:%v:%v", hem.key, hem.exact)
|
||||
}
|
||||
|
||||
// HeaderRegexMatcher matches on whether the entire request header value matches
|
||||
// the regex.
|
||||
type HeaderRegexMatcher struct {
|
||||
key string
|
||||
re *regexp.Regexp
|
||||
invert bool
|
||||
}
|
||||
|
||||
// NewHeaderRegexMatcher returns a new HeaderRegexMatcher.
|
||||
func NewHeaderRegexMatcher(key string, re *regexp.Regexp, invert bool) *HeaderRegexMatcher {
|
||||
return &HeaderRegexMatcher{key: key, re: re, invert: invert}
|
||||
}
|
||||
|
||||
// Match returns whether the passed in HTTP Headers match according to the
|
||||
// HeaderRegexMatcher.
|
||||
func (hrm *HeaderRegexMatcher) Match(md metadata.MD) bool {
|
||||
v, ok := valueFromMD(md, hrm.key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hrm.re.MatchString(v) != hrm.invert
|
||||
}
|
||||
|
||||
func (hrm *HeaderRegexMatcher) String() string {
|
||||
return fmt.Sprintf("headerRegex:%v:%v", hrm.key, hrm.re.String())
|
||||
}
|
||||
|
||||
// HeaderRangeMatcher matches on whether the request header value is within the
|
||||
// range. The header value must be an integer in base 10 notation.
|
||||
type HeaderRangeMatcher struct {
|
||||
key string
|
||||
start, end int64 // represents [start, end).
|
||||
invert bool
|
||||
}
|
||||
|
||||
// NewHeaderRangeMatcher returns a new HeaderRangeMatcher.
|
||||
func NewHeaderRangeMatcher(key string, start, end int64, invert bool) *HeaderRangeMatcher {
|
||||
return &HeaderRangeMatcher{key: key, start: start, end: end, invert: invert}
|
||||
}
|
||||
|
||||
// Match returns whether the passed in HTTP Headers match according to the
|
||||
// HeaderRangeMatcher.
|
||||
func (hrm *HeaderRangeMatcher) Match(md metadata.MD) bool {
|
||||
v, ok := valueFromMD(md, hrm.key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if i, err := strconv.ParseInt(v, 10, 64); err == nil && i >= hrm.start && i < hrm.end {
|
||||
return !hrm.invert
|
||||
}
|
||||
return hrm.invert
|
||||
}
|
||||
|
||||
func (hrm *HeaderRangeMatcher) String() string {
|
||||
return fmt.Sprintf("headerRange:%v:[%d,%d)", hrm.key, hrm.start, hrm.end)
|
||||
}
|
||||
|
||||
// HeaderPresentMatcher will match based on whether the header is present in the
|
||||
// whole request.
|
||||
type HeaderPresentMatcher struct {
|
||||
key string
|
||||
present bool
|
||||
}
|
||||
|
||||
// NewHeaderPresentMatcher returns a new HeaderPresentMatcher.
|
||||
func NewHeaderPresentMatcher(key string, present bool, invert bool) *HeaderPresentMatcher {
|
||||
if invert {
|
||||
present = !present
|
||||
}
|
||||
return &HeaderPresentMatcher{key: key, present: present}
|
||||
}
|
||||
|
||||
// Match returns whether the passed in HTTP Headers match according to the
|
||||
// HeaderPresentMatcher.
|
||||
func (hpm *HeaderPresentMatcher) Match(md metadata.MD) bool {
|
||||
vs, ok := valueFromMD(md, hpm.key)
|
||||
present := ok && len(vs) > 0 // TODO: Are we sure we need this len(vs) > 0?
|
||||
return present == hpm.present
|
||||
}
|
||||
|
||||
func (hpm *HeaderPresentMatcher) String() string {
|
||||
return fmt.Sprintf("headerPresent:%v:%v", hpm.key, hpm.present)
|
||||
}
|
||||
|
||||
// HeaderPrefixMatcher matches on whether the prefix of the header value matches
|
||||
// the prefix passed into this struct.
|
||||
type HeaderPrefixMatcher struct {
|
||||
key string
|
||||
prefix string
|
||||
invert bool
|
||||
}
|
||||
|
||||
// NewHeaderPrefixMatcher returns a new HeaderPrefixMatcher.
|
||||
func NewHeaderPrefixMatcher(key string, prefix string, invert bool) *HeaderPrefixMatcher {
|
||||
return &HeaderPrefixMatcher{key: key, prefix: prefix, invert: invert}
|
||||
}
|
||||
|
||||
// Match returns whether the passed in HTTP Headers match according to the
|
||||
// HeaderPrefixMatcher.
|
||||
func (hpm *HeaderPrefixMatcher) Match(md metadata.MD) bool {
|
||||
v, ok := valueFromMD(md, hpm.key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return strings.HasPrefix(v, hpm.prefix) != hpm.invert
|
||||
}
|
||||
|
||||
func (hpm *HeaderPrefixMatcher) String() string {
|
||||
return fmt.Sprintf("headerPrefix:%v:%v", hpm.key, hpm.prefix)
|
||||
}
|
||||
|
||||
// HeaderSuffixMatcher matches on whether the suffix of the header value matches
|
||||
// the suffix passed into this struct.
|
||||
type HeaderSuffixMatcher struct {
|
||||
key string
|
||||
suffix string
|
||||
invert bool
|
||||
}
|
||||
|
||||
// NewHeaderSuffixMatcher returns a new HeaderSuffixMatcher.
|
||||
func NewHeaderSuffixMatcher(key string, suffix string, invert bool) *HeaderSuffixMatcher {
|
||||
return &HeaderSuffixMatcher{key: key, suffix: suffix, invert: invert}
|
||||
}
|
||||
|
||||
// Match returns whether the passed in HTTP Headers match according to the
|
||||
// HeaderSuffixMatcher.
|
||||
func (hsm *HeaderSuffixMatcher) Match(md metadata.MD) bool {
|
||||
v, ok := valueFromMD(md, hsm.key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return strings.HasSuffix(v, hsm.suffix) != hsm.invert
|
||||
}
|
||||
|
||||
func (hsm *HeaderSuffixMatcher) String() string {
|
||||
return fmt.Sprintf("headerSuffix:%v:%v", hsm.key, hsm.suffix)
|
||||
}
|
||||
|
||||
// HeaderContainsMatcher matches on whether the header value contains the
|
||||
// value passed into this struct.
|
||||
type HeaderContainsMatcher struct {
|
||||
key string
|
||||
contains string
|
||||
invert bool
|
||||
}
|
||||
|
||||
// NewHeaderContainsMatcher returns a new HeaderContainsMatcher. key is the HTTP
|
||||
// Header key to match on, and contains is the value that the header should
|
||||
// contain for a successful match. An empty contains string does not
|
||||
// work, use HeaderPresentMatcher in that case.
|
||||
func NewHeaderContainsMatcher(key string, contains string, invert bool) *HeaderContainsMatcher {
|
||||
return &HeaderContainsMatcher{key: key, contains: contains, invert: invert}
|
||||
}
|
||||
|
||||
// Match returns whether the passed in HTTP Headers match according to the
|
||||
// HeaderContainsMatcher.
|
||||
func (hcm *HeaderContainsMatcher) Match(md metadata.MD) bool {
|
||||
v, ok := valueFromMD(md, hcm.key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(v, hcm.contains) != hcm.invert
|
||||
}
|
||||
|
||||
func (hcm *HeaderContainsMatcher) String() string {
|
||||
return fmt.Sprintf("headerContains:%v%v", hcm.key, hcm.contains)
|
||||
}
|
||||
|
||||
// HeaderStringMatcher matches on whether the header value matches against the
|
||||
// StringMatcher specified.
|
||||
type HeaderStringMatcher struct {
|
||||
key string
|
||||
stringMatcher StringMatcher
|
||||
invert bool
|
||||
}
|
||||
|
||||
// NewHeaderStringMatcher returns a new HeaderStringMatcher.
|
||||
func NewHeaderStringMatcher(key string, sm StringMatcher, invert bool) *HeaderStringMatcher {
|
||||
return &HeaderStringMatcher{
|
||||
key: key,
|
||||
stringMatcher: sm,
|
||||
invert: invert,
|
||||
}
|
||||
}
|
||||
|
||||
// Match returns whether the passed in HTTP Headers match according to the
|
||||
// specified StringMatcher.
|
||||
func (hsm *HeaderStringMatcher) Match(md metadata.MD) bool {
|
||||
v, ok := valueFromMD(md, hsm.key)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return hsm.stringMatcher.Match(v) != hsm.invert
|
||||
}
|
||||
|
||||
func (hsm *HeaderStringMatcher) String() string {
|
||||
return fmt.Sprintf("headerString:%v:%v", hsm.key, hsm.stringMatcher)
|
||||
}
|
||||
549
internal/xds/matcher/matcher_header_test.go
Normal file
549
internal/xds/matcher/matcher_header_test.go
Normal file
@@ -0,0 +1,549 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2020 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func TestHeaderExactMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key, exact string
|
||||
md metadata.MD
|
||||
want bool
|
||||
invert bool
|
||||
}{
|
||||
{
|
||||
name: "one value one match",
|
||||
key: "th",
|
||||
exact: "tv",
|
||||
md: metadata.Pairs("th", "tv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "two value one match",
|
||||
key: "th",
|
||||
exact: "tv",
|
||||
md: metadata.Pairs("th", "abc", "th", "tv"),
|
||||
// Doesn't match comma-concatenated string.
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "two value match concatenated",
|
||||
key: "th",
|
||||
exact: "abc,tv",
|
||||
md: metadata.Pairs("th", "abc", "th", "tv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not match",
|
||||
key: "th",
|
||||
exact: "tv",
|
||||
md: metadata.Pairs("th", "abc"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "invert header not present",
|
||||
key: "th",
|
||||
exact: "tv",
|
||||
md: metadata.Pairs(":method", "GET"),
|
||||
want: false,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
name: "invert header match",
|
||||
key: "th",
|
||||
exact: "tv",
|
||||
md: metadata.Pairs("th", "tv"),
|
||||
want: false,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
name: "invert header not match",
|
||||
key: "th",
|
||||
exact: "tv",
|
||||
md: metadata.Pairs("th", "tvv"),
|
||||
want: true,
|
||||
invert: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hem := NewHeaderExactMatcher(tt.key, tt.exact, tt.invert)
|
||||
if got := hem.Match(tt.md); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderRegexMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key, regexStr string
|
||||
md metadata.MD
|
||||
want bool
|
||||
invert bool
|
||||
}{
|
||||
{
|
||||
name: "one value one match",
|
||||
key: "th",
|
||||
regexStr: "^t+v*$",
|
||||
md: metadata.Pairs("th", "tttvv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "two value one match",
|
||||
key: "th",
|
||||
regexStr: "^t+v*$",
|
||||
md: metadata.Pairs("th", "abc", "th", "tttvv"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "two value match concatenated",
|
||||
key: "th",
|
||||
regexStr: "^[abc]*,t+v*$",
|
||||
md: metadata.Pairs("th", "abc", "th", "tttvv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "no match",
|
||||
key: "th",
|
||||
regexStr: "^t+v*$",
|
||||
md: metadata.Pairs("th", "abc"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "no match because only part of value matches with regex",
|
||||
key: "header",
|
||||
regexStr: "^a+$",
|
||||
md: metadata.Pairs("header", "ab"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "match because full value matches with regex",
|
||||
key: "header",
|
||||
regexStr: "^a+$",
|
||||
md: metadata.Pairs("header", "aa"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "invert header not present",
|
||||
key: "th",
|
||||
regexStr: "^t+v*$",
|
||||
md: metadata.Pairs(":method", "GET"),
|
||||
want: false,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
name: "invert header match",
|
||||
key: "th",
|
||||
regexStr: "^t+v*$",
|
||||
md: metadata.Pairs("th", "tttvv"),
|
||||
want: false,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
name: "invert header not match",
|
||||
key: "th",
|
||||
regexStr: "^t+v*$",
|
||||
md: metadata.Pairs("th", "abc"),
|
||||
want: true,
|
||||
invert: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hrm := NewHeaderRegexMatcher(tt.key, regexp.MustCompile(tt.regexStr), tt.invert)
|
||||
if got := hrm.Match(tt.md); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderRangeMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
start, end int64
|
||||
md metadata.MD
|
||||
want bool
|
||||
invert bool
|
||||
}{
|
||||
{
|
||||
name: "match",
|
||||
key: "th",
|
||||
start: 1, end: 10,
|
||||
md: metadata.Pairs("th", "5"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal to start",
|
||||
key: "th",
|
||||
start: 1, end: 10,
|
||||
md: metadata.Pairs("th", "1"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "equal to end",
|
||||
key: "th",
|
||||
start: 1, end: 10,
|
||||
md: metadata.Pairs("th", "10"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "negative",
|
||||
key: "th",
|
||||
start: -10, end: 10,
|
||||
md: metadata.Pairs("th", "-5"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "invert header not present",
|
||||
key: "th",
|
||||
start: 1, end: 10,
|
||||
md: metadata.Pairs(":method", "GET"),
|
||||
want: false,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
name: "invert header match",
|
||||
key: "th",
|
||||
start: 1, end: 10,
|
||||
md: metadata.Pairs("th", "5"),
|
||||
want: false,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
name: "invert header not match",
|
||||
key: "th",
|
||||
start: 1, end: 9,
|
||||
md: metadata.Pairs("th", "10"),
|
||||
want: true,
|
||||
invert: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hrm := NewHeaderRangeMatcher(tt.key, tt.start, tt.end, tt.invert)
|
||||
if got := hrm.Match(tt.md); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderPresentMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
present bool
|
||||
md metadata.MD
|
||||
want bool
|
||||
invert bool
|
||||
}{
|
||||
{
|
||||
name: "want present is present",
|
||||
key: "th",
|
||||
present: true,
|
||||
md: metadata.Pairs("th", "tv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "want present not present",
|
||||
key: "th",
|
||||
present: true,
|
||||
md: metadata.Pairs("abc", "tv"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "want not present is present",
|
||||
key: "th",
|
||||
present: false,
|
||||
md: metadata.Pairs("th", "tv"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "want not present is not present",
|
||||
key: "th",
|
||||
present: false,
|
||||
md: metadata.Pairs("abc", "tv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "invert header not present",
|
||||
key: "th",
|
||||
present: true,
|
||||
md: metadata.Pairs(":method", "GET"),
|
||||
want: true,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
name: "invert header match",
|
||||
key: "th",
|
||||
present: true,
|
||||
md: metadata.Pairs("th", "tv"),
|
||||
want: false,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
name: "invert header not match",
|
||||
key: "th",
|
||||
present: true,
|
||||
md: metadata.Pairs(":method", "GET"),
|
||||
want: true,
|
||||
invert: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hpm := NewHeaderPresentMatcher(tt.key, tt.present, tt.invert)
|
||||
if got := hpm.Match(tt.md); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderPrefixMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key, prefix string
|
||||
md metadata.MD
|
||||
want bool
|
||||
invert bool
|
||||
}{
|
||||
{
|
||||
name: "one value one match",
|
||||
key: "th",
|
||||
prefix: "tv",
|
||||
md: metadata.Pairs("th", "tv123"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "two value one match",
|
||||
key: "th",
|
||||
prefix: "tv",
|
||||
md: metadata.Pairs("th", "abc", "th", "tv123"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "two value match concatenated",
|
||||
key: "th",
|
||||
prefix: "tv",
|
||||
md: metadata.Pairs("th", "tv123", "th", "abc"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not match",
|
||||
key: "th",
|
||||
prefix: "tv",
|
||||
md: metadata.Pairs("th", "abc"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "invert header not present",
|
||||
key: "th",
|
||||
prefix: "tv",
|
||||
md: metadata.Pairs(":method", "GET"),
|
||||
want: false,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
name: "invert header match",
|
||||
key: "th",
|
||||
prefix: "tv",
|
||||
md: metadata.Pairs("th", "tv123"),
|
||||
want: false,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
name: "invert header not match",
|
||||
key: "th",
|
||||
prefix: "tv",
|
||||
md: metadata.Pairs("th", "abc"),
|
||||
want: true,
|
||||
invert: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hpm := NewHeaderPrefixMatcher(tt.key, tt.prefix, tt.invert)
|
||||
if got := hpm.Match(tt.md); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderSuffixMatcherMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key, suffix string
|
||||
md metadata.MD
|
||||
want bool
|
||||
invert bool
|
||||
}{
|
||||
{
|
||||
name: "one value one match",
|
||||
key: "th",
|
||||
suffix: "tv",
|
||||
md: metadata.Pairs("th", "123tv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "two value one match",
|
||||
key: "th",
|
||||
suffix: "tv",
|
||||
md: metadata.Pairs("th", "123tv", "th", "abc"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "two value match concatenated",
|
||||
key: "th",
|
||||
suffix: "tv",
|
||||
md: metadata.Pairs("th", "abc", "th", "123tv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not match",
|
||||
key: "th",
|
||||
suffix: "tv",
|
||||
md: metadata.Pairs("th", "abc"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "invert header not present",
|
||||
key: "th",
|
||||
suffix: "tv",
|
||||
md: metadata.Pairs(":method", "GET"),
|
||||
want: false,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
name: "invert header match",
|
||||
key: "th",
|
||||
suffix: "tv",
|
||||
md: metadata.Pairs("th", "123tv"),
|
||||
want: false,
|
||||
invert: true,
|
||||
},
|
||||
{
|
||||
name: "invert header not match",
|
||||
key: "th",
|
||||
suffix: "tv",
|
||||
md: metadata.Pairs("th", "abc"),
|
||||
want: true,
|
||||
invert: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
hsm := NewHeaderSuffixMatcher(tt.key, tt.suffix, tt.invert)
|
||||
if got := hsm.Match(tt.md); got != tt.want {
|
||||
t.Errorf("match() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeaderStringMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
sm StringMatcher
|
||||
invert bool
|
||||
md metadata.MD
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "should-match",
|
||||
key: "th",
|
||||
sm: StringMatcher{
|
||||
exactMatch: newStringP("tv"),
|
||||
},
|
||||
invert: false,
|
||||
md: metadata.Pairs("th", "tv"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not match",
|
||||
key: "th",
|
||||
sm: StringMatcher{
|
||||
containsMatch: newStringP("tv"),
|
||||
},
|
||||
invert: false,
|
||||
md: metadata.Pairs("th", "not-match"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "invert string match",
|
||||
key: "th",
|
||||
sm: StringMatcher{
|
||||
containsMatch: newStringP("tv"),
|
||||
},
|
||||
invert: true,
|
||||
md: metadata.Pairs("th", "not-match"),
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "header missing",
|
||||
key: "th",
|
||||
sm: StringMatcher{
|
||||
containsMatch: newStringP("tv"),
|
||||
},
|
||||
invert: false,
|
||||
md: metadata.Pairs("not-specified-key", "not-match"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "header missing invert true",
|
||||
key: "th",
|
||||
sm: StringMatcher{
|
||||
containsMatch: newStringP("tv"),
|
||||
},
|
||||
invert: true,
|
||||
md: metadata.Pairs("not-specified-key", "not-match"),
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "header empty string invert",
|
||||
key: "th",
|
||||
sm: StringMatcher{
|
||||
containsMatch: newStringP("tv"),
|
||||
},
|
||||
invert: true,
|
||||
md: metadata.Pairs("th", ""),
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
hsm := NewHeaderStringMatcher(test.key, test.sm, test.invert)
|
||||
if got := hsm.Match(test.md); got != test.want {
|
||||
t.Errorf("match() = %v, want %v", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
229
internal/xds/matcher/string_matcher.go
Normal file
229
internal/xds/matcher/string_matcher.go
Normal file
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
// Package matcher contains types that need to be shared between code under
|
||||
// google.golang.org/grpc/xds/... and the rest of gRPC.
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
|
||||
)
|
||||
|
||||
// StringMatcher contains match criteria for matching a string, and is an
|
||||
// internal representation of the `StringMatcher` proto defined at
|
||||
// https://github.com/envoyproxy/envoy/blob/main/api/envoy/type/matcher/v3/string.proto.
|
||||
type StringMatcher struct {
|
||||
// Since these match fields are part of a `oneof` in the corresponding xDS
|
||||
// proto, only one of them is expected to be set.
|
||||
exactMatch *string
|
||||
prefixMatch *string
|
||||
suffixMatch *string
|
||||
regexMatch *regexp.Regexp
|
||||
containsMatch *string
|
||||
// If true, indicates the exact/prefix/suffix/contains matching should be
|
||||
// case insensitive. This has no effect on the regex match.
|
||||
ignoreCase bool
|
||||
}
|
||||
|
||||
// Match returns true if input matches the criteria in the given StringMatcher.
|
||||
func (sm StringMatcher) Match(input string) bool {
|
||||
switch {
|
||||
case sm.exactMatch != nil:
|
||||
if sm.ignoreCase {
|
||||
input = strings.ToLower(input)
|
||||
}
|
||||
return input == *sm.exactMatch
|
||||
case sm.prefixMatch != nil:
|
||||
if sm.ignoreCase {
|
||||
input = strings.ToLower(input)
|
||||
}
|
||||
return strings.HasPrefix(input, *sm.prefixMatch)
|
||||
case sm.suffixMatch != nil:
|
||||
if sm.ignoreCase {
|
||||
input = strings.ToLower(input)
|
||||
}
|
||||
return strings.HasSuffix(input, *sm.suffixMatch)
|
||||
case sm.containsMatch != nil:
|
||||
if sm.ignoreCase {
|
||||
input = strings.ToLower(input)
|
||||
}
|
||||
return strings.Contains(input, *sm.containsMatch)
|
||||
case sm.regexMatch != nil:
|
||||
return sm.regexMatch.MatchString(input)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// newStrPtr allocates a new string that holds the value of input and returns a
|
||||
// pointer to it. ignoreCase controls if a lower case version of input is used.
|
||||
func newStrPtr(input *string, ignoreCase bool) *string {
|
||||
if input == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
s := new(string)
|
||||
if ignoreCase {
|
||||
*s = strings.ToLower(*input)
|
||||
} else {
|
||||
*s = *input
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// StringMatcherFromProto is a helper function to create a StringMatcher from
|
||||
// the corresponding StringMatcher proto.
|
||||
//
|
||||
// Returns a non-nil error if matcherProto is invalid.
|
||||
func StringMatcherFromProto(matcherProto *v3matcherpb.StringMatcher) (StringMatcher, error) {
|
||||
if matcherProto == nil {
|
||||
return StringMatcher{}, errors.New("input StringMatcher proto is nil")
|
||||
}
|
||||
|
||||
matcher := StringMatcher{ignoreCase: matcherProto.GetIgnoreCase()}
|
||||
switch mt := matcherProto.GetMatchPattern().(type) {
|
||||
case *v3matcherpb.StringMatcher_Exact:
|
||||
matcher.exactMatch = newStrPtr(&mt.Exact, matcher.ignoreCase)
|
||||
case *v3matcherpb.StringMatcher_Prefix:
|
||||
if matcherProto.GetPrefix() == "" {
|
||||
return StringMatcher{}, errors.New("empty prefix is not allowed in StringMatcher")
|
||||
}
|
||||
matcher.prefixMatch = newStrPtr(&mt.Prefix, matcher.ignoreCase)
|
||||
case *v3matcherpb.StringMatcher_Suffix:
|
||||
if matcherProto.GetSuffix() == "" {
|
||||
return StringMatcher{}, errors.New("empty suffix is not allowed in StringMatcher")
|
||||
}
|
||||
matcher.suffixMatch = newStrPtr(&mt.Suffix, matcher.ignoreCase)
|
||||
case *v3matcherpb.StringMatcher_SafeRegex:
|
||||
regex := matcherProto.GetSafeRegex().GetRegex()
|
||||
re, err := CompileSafeRegex(regex)
|
||||
if err != nil {
|
||||
return StringMatcher{}, fmt.Errorf("safe_regex matcher %q is invalid", regex)
|
||||
}
|
||||
matcher = NewRegexStringMatcher(re)
|
||||
case *v3matcherpb.StringMatcher_Contains:
|
||||
if matcherProto.GetContains() == "" {
|
||||
return StringMatcher{}, errors.New("empty contains is not allowed in StringMatcher")
|
||||
}
|
||||
matcher.containsMatch = newStrPtr(&mt.Contains, matcher.ignoreCase)
|
||||
default:
|
||||
return StringMatcher{}, fmt.Errorf("unrecognized string matcher: %+v", matcherProto)
|
||||
}
|
||||
return matcher, nil
|
||||
}
|
||||
|
||||
// NewExactStringMatcher creates a string matcher that requires the input string
|
||||
// to exactly match the pattern specified here. The match will be case
|
||||
// insensitive if ignore_case is true.
|
||||
func NewExactStringMatcher(pattern string, ignoreCase bool) StringMatcher {
|
||||
return StringMatcher{
|
||||
exactMatch: newStrPtr(&pattern, ignoreCase),
|
||||
ignoreCase: ignoreCase,
|
||||
}
|
||||
}
|
||||
|
||||
// NewPrefixStringMatcher creates a string matcher that requires the input
|
||||
// string to contain the prefix specified here. The match will be case
|
||||
// insensitive if ignore_case is true.
|
||||
func NewPrefixStringMatcher(prefix string, ignoreCase bool) StringMatcher {
|
||||
return StringMatcher{
|
||||
prefixMatch: newStrPtr(&prefix, ignoreCase),
|
||||
ignoreCase: ignoreCase,
|
||||
}
|
||||
}
|
||||
|
||||
// NewSuffixStringMatcher creates a string matcher that requires the input
|
||||
// string to contain the suffix specified here. The match will be case
|
||||
// insensitive if ignore_case is true.
|
||||
func NewSuffixStringMatcher(suffix string, ignoreCase bool) StringMatcher {
|
||||
return StringMatcher{
|
||||
suffixMatch: newStrPtr(&suffix, ignoreCase),
|
||||
ignoreCase: ignoreCase,
|
||||
}
|
||||
}
|
||||
|
||||
// NewContainsStringMatcher creates a string matcher that requires the input
|
||||
// string to contain the pattern specified here. The match will be case
|
||||
// insensitive if ignore_case is true.
|
||||
func NewContainsStringMatcher(pattern string, ignoreCase bool) StringMatcher {
|
||||
return StringMatcher{
|
||||
containsMatch: newStrPtr(&pattern, ignoreCase),
|
||||
ignoreCase: ignoreCase,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRegexStringMatcher creates a string matcher that requires the input string
|
||||
// to match the regular expression specified here.
|
||||
func NewRegexStringMatcher(regex *regexp.Regexp) StringMatcher {
|
||||
return StringMatcher{
|
||||
regexMatch: regex,
|
||||
}
|
||||
}
|
||||
|
||||
// ExactMatch returns the value of the configured exact match or an empty string
|
||||
// if exact match criteria was not specified.
|
||||
func (sm StringMatcher) ExactMatch() string {
|
||||
if sm.exactMatch != nil {
|
||||
return *sm.exactMatch
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Equal returns true if other and sm are equivalent to each other.
|
||||
func (sm StringMatcher) Equal(other StringMatcher) bool {
|
||||
if sm.ignoreCase != other.ignoreCase {
|
||||
return false
|
||||
}
|
||||
|
||||
if (sm.exactMatch != nil) != (other.exactMatch != nil) ||
|
||||
(sm.prefixMatch != nil) != (other.prefixMatch != nil) ||
|
||||
(sm.suffixMatch != nil) != (other.suffixMatch != nil) ||
|
||||
(sm.regexMatch != nil) != (other.regexMatch != nil) ||
|
||||
(sm.containsMatch != nil) != (other.containsMatch != nil) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch {
|
||||
case sm.exactMatch != nil:
|
||||
return *sm.exactMatch == *other.exactMatch
|
||||
case sm.prefixMatch != nil:
|
||||
return *sm.prefixMatch == *other.prefixMatch
|
||||
case sm.suffixMatch != nil:
|
||||
return *sm.suffixMatch == *other.suffixMatch
|
||||
case sm.regexMatch != nil:
|
||||
return sm.regexMatch.String() == other.regexMatch.String()
|
||||
case sm.containsMatch != nil:
|
||||
return *sm.containsMatch == *other.containsMatch
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// CompileSafeRegex attempts to compile the provided pattern as a regular
|
||||
// expression. It first compiles the unanchored pattern to catch syntax errors
|
||||
// and then compiles and returns the explicitly anchored pattern to guarantee
|
||||
// full-string matching.
|
||||
func CompileSafeRegex(pattern string) (*regexp.Regexp, error) {
|
||||
if _, err := regexp.Compile(pattern); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return regexp.Compile(fmt.Sprintf("^(?:%s)$", pattern))
|
||||
}
|
||||
349
internal/xds/matcher/string_matcher_test.go
Normal file
349
internal/xds/matcher/string_matcher_test.go
Normal file
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2021 gRPC authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package matcher
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
v3matcherpb "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func TestStringMatcherFromProto(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
inputProto *v3matcherpb.StringMatcher
|
||||
wantMatcher StringMatcher
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "nil proto",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "empty prefix",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: ""},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "empty suffix",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: ""},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "empty contains",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Contains{Contains: ""},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "invalid regex",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{
|
||||
SafeRegex: &v3matcherpb.RegexMatcher{Regex: "??"},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
desc: "happy case exact",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "exact"},
|
||||
},
|
||||
wantMatcher: StringMatcher{exactMatch: newStringP("exact")},
|
||||
},
|
||||
{
|
||||
desc: "happy case exact ignore case",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Exact{Exact: "EXACT"},
|
||||
IgnoreCase: true,
|
||||
},
|
||||
wantMatcher: StringMatcher{
|
||||
exactMatch: newStringP("exact"),
|
||||
ignoreCase: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "happy case prefix",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "prefix"},
|
||||
},
|
||||
wantMatcher: StringMatcher{prefixMatch: newStringP("prefix")},
|
||||
},
|
||||
{
|
||||
desc: "happy case prefix ignore case",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Prefix{Prefix: "PREFIX"},
|
||||
IgnoreCase: true,
|
||||
},
|
||||
wantMatcher: StringMatcher{
|
||||
prefixMatch: newStringP("prefix"),
|
||||
ignoreCase: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "happy case suffix",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: "suffix"},
|
||||
},
|
||||
wantMatcher: StringMatcher{suffixMatch: newStringP("suffix")},
|
||||
},
|
||||
{
|
||||
desc: "happy case suffix ignore case",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Suffix{Suffix: "SUFFIX"},
|
||||
IgnoreCase: true,
|
||||
},
|
||||
wantMatcher: StringMatcher{
|
||||
suffixMatch: newStringP("suffix"),
|
||||
ignoreCase: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "happy case regex",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{
|
||||
SafeRegex: &v3matcherpb.RegexMatcher{Regex: "good?regex?"},
|
||||
},
|
||||
},
|
||||
wantMatcher: StringMatcher{regexMatch: regexp.MustCompile("^(?:good?regex?)$")},
|
||||
},
|
||||
{
|
||||
desc: "regex with ignore case",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_SafeRegex{
|
||||
SafeRegex: &v3matcherpb.RegexMatcher{Regex: "good?regex?"},
|
||||
},
|
||||
IgnoreCase: true,
|
||||
},
|
||||
wantMatcher: StringMatcher{
|
||||
regexMatch: regexp.MustCompile("^(?:good?regex?)$"),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "happy case contains",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Contains{Contains: "contains"},
|
||||
},
|
||||
wantMatcher: StringMatcher{containsMatch: newStringP("contains")},
|
||||
},
|
||||
{
|
||||
desc: "happy case contains ignore case",
|
||||
inputProto: &v3matcherpb.StringMatcher{
|
||||
MatchPattern: &v3matcherpb.StringMatcher_Contains{Contains: "CONTAINS"},
|
||||
IgnoreCase: true,
|
||||
},
|
||||
wantMatcher: StringMatcher{
|
||||
containsMatch: newStringP("contains"),
|
||||
ignoreCase: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
gotMatcher, err := StringMatcherFromProto(test.inputProto)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Fatalf("StringMatcherFromProto(%+v) returned err: %v, wantErr: %v", test.inputProto, err, test.wantErr)
|
||||
}
|
||||
if diff := cmp.Diff(gotMatcher, test.wantMatcher, cmp.AllowUnexported(regexp.Regexp{})); diff != "" {
|
||||
t.Fatalf("StringMatcherFromProto(%+v) returned unexpected diff (-got, +want):\n%s", test.inputProto, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
var (
|
||||
exactMatcher = NewExactStringMatcher("exact", false)
|
||||
exactMatcherIgnoreCase = NewExactStringMatcher("exact", true)
|
||||
prefixMatcher = NewPrefixStringMatcher("prefix", false)
|
||||
prefixMatcherIgnoreCase = NewPrefixStringMatcher("prefix", true)
|
||||
suffixMatcher = NewSuffixStringMatcher("suffix", false)
|
||||
suffixMatcherIgnoreCase = NewSuffixStringMatcher("suffix", true)
|
||||
containsMatcher = NewContainsStringMatcher("contains", false)
|
||||
containsMatcherIgnoreCase = NewContainsStringMatcher("contains", true)
|
||||
regexMatcher = NewRegexStringMatcher(regexp.MustCompile("^(?:good?regex?)$"))
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
matcher StringMatcher
|
||||
input string
|
||||
wantMatch bool
|
||||
}{
|
||||
{
|
||||
desc: "exact match success",
|
||||
matcher: exactMatcher,
|
||||
input: "exact",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
desc: "exact match failure",
|
||||
matcher: exactMatcher,
|
||||
input: "not-exact",
|
||||
},
|
||||
{
|
||||
desc: "exact match success with ignore case",
|
||||
matcher: exactMatcherIgnoreCase,
|
||||
input: "EXACT",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
desc: "exact match failure with ignore case",
|
||||
matcher: exactMatcherIgnoreCase,
|
||||
input: "not-exact",
|
||||
},
|
||||
{
|
||||
desc: "prefix match success",
|
||||
matcher: prefixMatcher,
|
||||
input: "prefixIsHere",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
desc: "prefix match failure",
|
||||
matcher: prefixMatcher,
|
||||
input: "not-prefix",
|
||||
},
|
||||
{
|
||||
desc: "prefix match success with ignore case",
|
||||
matcher: prefixMatcherIgnoreCase,
|
||||
input: "PREFIXisHere",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
desc: "prefix match failure with ignore case",
|
||||
matcher: prefixMatcherIgnoreCase,
|
||||
input: "not-PREFIX",
|
||||
},
|
||||
{
|
||||
desc: "suffix match success",
|
||||
matcher: suffixMatcher,
|
||||
input: "hereIsThesuffix",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
desc: "suffix match failure",
|
||||
matcher: suffixMatcher,
|
||||
input: "suffix-is-not-here",
|
||||
},
|
||||
{
|
||||
desc: "suffix match success with ignore case",
|
||||
matcher: suffixMatcherIgnoreCase,
|
||||
input: "hereIsTheSuFFix",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
desc: "suffix match failure with ignore case",
|
||||
matcher: suffixMatcherIgnoreCase,
|
||||
input: "SUFFIX-is-not-here",
|
||||
},
|
||||
{
|
||||
desc: "regex match success",
|
||||
matcher: regexMatcher,
|
||||
input: "goodregex",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
desc: "regex match failure because only part match",
|
||||
matcher: regexMatcher,
|
||||
input: "goodregexa",
|
||||
wantMatch: false,
|
||||
},
|
||||
{
|
||||
desc: "regex match failure",
|
||||
matcher: regexMatcher,
|
||||
input: "regex-is-not-here",
|
||||
},
|
||||
{
|
||||
desc: "contains match success",
|
||||
matcher: containsMatcher,
|
||||
input: "IScontainsHERE",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
desc: "contains match failure",
|
||||
matcher: containsMatcher,
|
||||
input: "con-tains-is-not-here",
|
||||
},
|
||||
{
|
||||
desc: "contains match success with ignore case",
|
||||
matcher: containsMatcherIgnoreCase,
|
||||
input: "isCONTAINShere",
|
||||
wantMatch: true,
|
||||
},
|
||||
{
|
||||
desc: "contains match failure with ignore case",
|
||||
matcher: containsMatcherIgnoreCase,
|
||||
input: "CON-TAINS-is-not-here",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
if gotMatch := test.matcher.Match(test.input); gotMatch != test.wantMatch {
|
||||
t.Errorf("StringMatcher.Match(%s) returned %v, want %v", test.input, gotMatch, test.wantMatch)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newStringP(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func TestCompileSafeRegex(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc string
|
||||
pattern string
|
||||
wantRegex string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
desc: "ValidPattern",
|
||||
pattern: "abc",
|
||||
wantRegex: "^(?:abc)$",
|
||||
},
|
||||
{
|
||||
desc: "UnmatchedParenthesis",
|
||||
pattern: "abc(",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
re, err := CompileSafeRegex(test.pattern)
|
||||
if (err != nil) != test.wantErr {
|
||||
t.Fatalf("CompileSafeRegex(%q) returned error %q, wantErr=%v", test.pattern, err, test.wantErr)
|
||||
}
|
||||
if err == nil {
|
||||
if re.String() != test.wantRegex {
|
||||
t.Fatalf("CompileSafeRegex(%q) = %q, want %q", test.pattern, re.String(), test.wantRegex)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user