Files
kserve/pkg/testing/httproute_matchers.go
2026-05-21 21:44:35 +00:00

217 lines
6.2 KiB
Go

/*
Copyright 2025 The KServe 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 testing
import (
"encoding/json"
"errors"
"fmt"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/utils/ptr"
"github.com/onsi/gomega/types"
gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
)
// extractHTTPRoute safely extracts HTTPRoute from either pointer or value type
func extractHTTPRoute(actual any) (*gwapiv1.HTTPRoute, error) {
switch v := actual.(type) {
case gwapiv1.HTTPRouteSpec:
return &gwapiv1.HTTPRoute{Spec: v}, nil
case *gwapiv1.HTTPRouteSpec:
if v == nil {
return nil, errors.New("expected non-nil gatewayapi.HTTPRouteSpec, but got nil")
}
return &gwapiv1.HTTPRoute{Spec: *v}, nil
case *gwapiv1.HTTPRoute:
if v == nil {
return nil, errors.New("expected non-nil gatewayapi.HTTPRoute, but got nil")
}
return v, nil
case gwapiv1.HTTPRoute:
return &v, nil
default:
return nil, fmt.Errorf("expected gatewayapi.HTTPRoute gatewayapi.HTTPRouteSpec, but got %T", actual)
}
}
// HaveGatewayRefs returns a matcher that checks if an HTTPRoute has the specified gateway parent refs
func HaveGatewayRefs(expectedGateways ...gwapiv1.ParentReference) types.GomegaMatcher {
return &haveGatewayRefsMatcher{
expected: expectedGateways,
}
}
type haveGatewayRefsMatcher struct {
expected []gwapiv1.ParentReference
actualParentRefs []gwapiv1.ParentReference
}
func (matcher *haveGatewayRefsMatcher) Match(actual any) (success bool, err error) {
httpRoute, err := extractHTTPRoute(actual)
if err != nil {
return false, err
}
matcher.actualParentRefs = httpRoute.Spec.ParentRefs
if len(matcher.actualParentRefs) != len(matcher.expected) {
return false, nil
}
for _, want := range matcher.expected {
found := false
for _, got := range matcher.actualParentRefs {
if parentRefMatches(want, got) {
found = true
break
}
}
if !found {
return false, nil
}
}
return true, nil
}
// parentRefMatches checks whether two ParentReferences match on Name, Namespace,
// and SectionName. Nil fields on the expected ref are treated as "don't care".
func parentRefMatches(want, got gwapiv1.ParentReference) bool {
if want.Name != got.Name {
return false
}
if want.Namespace != nil && ptr.Deref(want.Namespace, "") != ptr.Deref(got.Namespace, "") {
return false
}
if want.SectionName != nil && ptr.Deref(want.SectionName, "") != ptr.Deref(got.SectionName, "") {
return false
}
return true
}
func (matcher *haveGatewayRefsMatcher) FailureMessage(actual any) string {
return fmt.Sprintf("Expected %T to have gateway refs %v, but found %v",
actual, matcher.expected, matcher.actualParentRefs)
}
func (matcher *haveGatewayRefsMatcher) NegatedFailureMessage(actual any) string {
return fmt.Sprintf("Expected %T to not have gateway refs %v, but they were found",
actual, matcher.expected)
}
// HaveBackendRefs returns a matcher that checks if an HTTPRoute contains the specified backend refs.
// It uses "contains" semantics: the route may have additional backend refs beyond those listed.
func HaveBackendRefs(backends ...gwapiv1.HTTPBackendRef) types.GomegaMatcher {
return &haveBackendRefsMatcher{
expectedBackendRefs: backends,
}
}
type haveBackendRefsMatcher struct {
expectedBackendRefs []gwapiv1.HTTPBackendRef
actualBackendRefs []gwapiv1.HTTPBackendRef
}
func (matcher *haveBackendRefsMatcher) Match(actual any) (success bool, err error) {
httpRoute, err := extractHTTPRoute(actual)
if err != nil {
return false, err
}
for _, rule := range httpRoute.Spec.Rules {
matcher.actualBackendRefs = append(matcher.actualBackendRefs, rule.BackendRefs...)
}
for _, want := range matcher.expectedBackendRefs {
found := false
for _, got := range matcher.actualBackendRefs {
if equality.Semantic.DeepEqual(want, got) {
found = true
break
}
}
if !found {
return false, nil
}
}
return true, nil
}
func (matcher *haveBackendRefsMatcher) FailureMessage(actual any) string {
expected, _ := json.MarshalIndent(matcher.expectedBackendRefs, "", " ")
got, _ := json.MarshalIndent(matcher.actualBackendRefs, "", " ")
return fmt.Sprintf("Expected %T to have backend refs:\n%s\nbut found:\n%s",
actual, expected, got)
}
func (matcher *haveBackendRefsMatcher) NegatedFailureMessage(actual any) string {
expected, _ := json.MarshalIndent(matcher.expectedBackendRefs, "", " ")
got, _ := json.MarshalIndent(matcher.actualBackendRefs, "", " ")
return fmt.Sprintf("Expected %T to not have backend refs:\n%s, got:\n%s",
actual, expected, got)
}
// HaveHeaderMatch returns a matcher that checks if any rule in an HTTPRoute
// contains a match with the specified header name and value.
func HaveHeaderMatch(headerName, headerValue string) types.GomegaMatcher {
return &haveHeaderMatchMatcher{
headerName: headerName,
headerValue: headerValue,
}
}
type haveHeaderMatchMatcher struct {
headerName string
headerValue string
allHeaders []gwapiv1.HTTPHeaderMatch
}
func (m *haveHeaderMatchMatcher) Match(actual any) (bool, error) {
httpRoute, err := extractHTTPRoute(actual)
if err != nil {
return false, err
}
m.allHeaders = nil
for _, rule := range httpRoute.Spec.Rules {
for _, match := range rule.Matches {
for _, h := range match.Headers {
m.allHeaders = append(m.allHeaders, h)
if string(h.Name) == m.headerName && h.Value == m.headerValue {
return true, nil
}
}
}
}
return false, nil
}
func (m *haveHeaderMatchMatcher) FailureMessage(actual any) string {
got, _ := json.MarshalIndent(m.allHeaders, "", " ")
return fmt.Sprintf("Expected %T to have header match %s=%s, but found:\n%s",
actual, m.headerName, m.headerValue, got)
}
func (m *haveHeaderMatchMatcher) NegatedFailureMessage(actual any) string {
return fmt.Sprintf("Expected %T to not have header match %s=%s, but it was found",
actual, m.headerName, m.headerValue)
}