Add ServiceEndpoints Mutation hook tests (#18404)

* Add ServiceEndpoints Mutation hook tests

* Move endpoint owner validation into the validation hook

Also there were some minor changes to error validation to account for go-cmp not liking to peer through an errors.errorstring type that get created by errors.New
This commit is contained in:
Matt Keeler 2023-08-08 15:22:14 -04:00 committed by GitHub
parent 43d8898e08
commit 91d331bbaa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 134 additions and 43 deletions

View File

@ -44,6 +44,16 @@ func MutateServiceEndpoints(res *pbresource.Resource) error {
} }
} }
return nil
}
func ValidateServiceEndpoints(res *pbresource.Resource) error {
var svcEndpoints pbcatalog.ServiceEndpoints
if err := res.Data.UnmarshalTo(&svcEndpoints); err != nil {
return resource.NewErrDataParse(&svcEndpoints, err)
}
var err error var err error
if !resource.EqualType(res.Owner.Type, ServiceV1Alpha1Type) { if !resource.EqualType(res.Owner.Type, ServiceV1Alpha1Type) {
err = multierror.Append(err, resource.ErrOwnerTypeInvalid{ err = multierror.Append(err, resource.ErrOwnerTypeInvalid{
@ -54,6 +64,7 @@ func MutateServiceEndpoints(res *pbresource.Resource) error {
if !resource.EqualTenancy(res.Owner.Tenancy, res.Id.Tenancy) { if !resource.EqualTenancy(res.Owner.Tenancy, res.Id.Tenancy) {
err = multierror.Append(err, resource.ErrOwnerTenantInvalid{ err = multierror.Append(err, resource.ErrOwnerTenantInvalid{
ResourceType: ServiceEndpointsV1Alpha1Type,
ResourceTenancy: res.Id.Tenancy, ResourceTenancy: res.Id.Tenancy,
OwnerTenancy: res.Owner.Tenancy, OwnerTenancy: res.Owner.Tenancy,
}) })
@ -69,17 +80,6 @@ func MutateServiceEndpoints(res *pbresource.Resource) error {
}) })
} }
return err
}
func ValidateServiceEndpoints(res *pbresource.Resource) error {
var svcEndpoints pbcatalog.ServiceEndpoints
if err := res.Data.UnmarshalTo(&svcEndpoints); err != nil {
return resource.NewErrDataParse(&svcEndpoints, err)
}
var err error
for idx, endpoint := range svcEndpoints.Endpoints { for idx, endpoint := range svcEndpoints.Endpoints {
if endpointErr := validateEndpoint(endpoint, res); endpointErr != nil { if endpointErr := validateEndpoint(endpoint, res); endpointErr != nil {
err = multierror.Append(err, resource.ErrInvalidListElement{ err = multierror.Append(err, resource.ErrInvalidListElement{

View File

@ -7,11 +7,10 @@ import (
"testing" "testing"
"github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/resource"
rtest "github.com/hashicorp/consul/internal/resource/resourcetest"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v1alpha1" pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v1alpha1"
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/anypb"
) )
var ( var (
@ -20,23 +19,14 @@ var (
Namespace: "default", Namespace: "default",
PeerName: "local", PeerName: "local",
} }
badEndpointTenancy = &pbresource.Tenancy{
Partition: "default",
Namespace: "bad",
PeerName: "local",
}
) )
func createServiceEndpointsResource(t *testing.T, data protoreflect.ProtoMessage) *pbresource.Resource {
res := &pbresource.Resource{
Id: &pbresource.ID{
Type: ServiceEndpointsType,
Tenancy: defaultEndpointTenancy,
Name: "test-service",
},
}
var err error
res.Data, err = anypb.New(data)
require.NoError(t, err)
return res
}
func TestValidateServiceEndpoints_Ok(t *testing.T) { func TestValidateServiceEndpoints_Ok(t *testing.T) {
data := &pbcatalog.ServiceEndpoints{ data := &pbcatalog.ServiceEndpoints{
Endpoints: []*pbcatalog.Endpoint{ Endpoints: []*pbcatalog.Endpoint{
@ -62,8 +52,14 @@ func TestValidateServiceEndpoints_Ok(t *testing.T) {
}, },
} }
res := createServiceEndpointsResource(t, data) res := rtest.Resource(ServiceEndpointsType, "test-service").
WithData(t, data).
Build()
// fill in owner automatically
require.NoError(t, MutateServiceEndpoints(res))
// Now validate that everything is good.
err := ValidateServiceEndpoints(res) err := ValidateServiceEndpoints(res)
require.NoError(t, err) require.NoError(t, err)
} }
@ -73,7 +69,7 @@ func TestValidateServiceEndpoints_ParseError(t *testing.T) {
// to cause the error we are expecting // to cause the error we are expecting
data := &pbcatalog.IP{Address: "198.18.0.1"} data := &pbcatalog.IP{Address: "198.18.0.1"}
res := createServiceEndpointsResource(t, data) res := rtest.Resource(ServiceEndpointsType, "test-service").WithData(t, data).Build()
err := ValidateServiceEndpoints(res) err := ValidateServiceEndpoints(res)
require.Error(t, err) require.Error(t, err)
@ -104,6 +100,7 @@ func TestValidateServiceEndpoints_EndpointInvalid(t *testing.T) {
} }
type testCase struct { type testCase struct {
owner *pbresource.ID
modify func(*pbcatalog.Endpoint) modify func(*pbcatalog.Endpoint)
validateErr func(t *testing.T, err error) validateErr func(t *testing.T, err error)
} }
@ -140,11 +137,11 @@ func TestValidateServiceEndpoints_EndpointInvalid(t *testing.T) {
} }
}, },
validateErr: func(t *testing.T, err error) { validateErr: func(t *testing.T, err error) {
var mapErr resource.ErrInvalidMapKey rtest.RequireError(t, err, resource.ErrInvalidMapKey{
require.ErrorAs(t, err, &mapErr) Map: "ports",
require.Equal(t, "ports", mapErr.Map) Key: "",
require.Equal(t, "", mapErr.Key) Wrapped: resource.ErrEmpty,
require.Equal(t, resource.ErrEmpty, mapErr.Wrapped) })
}, },
}, },
"port-0": { "port-0": {
@ -163,18 +160,50 @@ func TestValidateServiceEndpoints_EndpointInvalid(t *testing.T) {
require.ErrorIs(t, err, errInvalidPhysicalPort) require.ErrorIs(t, err, errInvalidPhysicalPort)
}, },
}, },
"invalid-owner": {
owner: &pbresource.ID{
Type: DNSPolicyType,
Tenancy: badEndpointTenancy,
Name: "wrong",
},
validateErr: func(t *testing.T, err error) {
rtest.RequireError(t, err, resource.ErrOwnerTypeInvalid{
ResourceType: ServiceEndpointsType,
OwnerType: DNSPolicyType})
rtest.RequireError(t, err, resource.ErrOwnerTenantInvalid{
ResourceType: ServiceEndpointsType,
ResourceTenancy: defaultEndpointTenancy,
OwnerTenancy: badEndpointTenancy,
})
rtest.RequireError(t, err, resource.ErrInvalidField{
Name: "name",
Wrapped: errInvalidEndpointsOwnerName{
Name: "test-service",
OwnerName: "wrong"},
})
},
},
} }
for name, tcase := range cases { for name, tcase := range cases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
data := genData() endpoint := genData()
tcase.modify(data) if tcase.modify != nil {
tcase.modify(endpoint)
}
res := createServiceEndpointsResource(t, &pbcatalog.ServiceEndpoints{ data := &pbcatalog.ServiceEndpoints{
Endpoints: []*pbcatalog.Endpoint{ Endpoints: []*pbcatalog.Endpoint{
data, endpoint,
}, },
}) }
res := rtest.Resource(ServiceEndpointsType, "test-service").
WithOwner(tcase.owner).
WithData(t, data).
Build()
// Run the mututation to setup defaults
require.NoError(t, MutateServiceEndpoints(res))
err := ValidateServiceEndpoints(res) err := ValidateServiceEndpoints(res)
require.Error(t, err) require.Error(t, err)
@ -182,3 +211,13 @@ func TestValidateServiceEndpoints_EndpointInvalid(t *testing.T) {
}) })
} }
} }
func TestMutateServiceEndpoints_PopulateOwner(t *testing.T) {
res := rtest.Resource(ServiceEndpointsType, "test-service").Build()
require.NoError(t, MutateServiceEndpoints(res))
require.NotNil(t, res.Owner)
require.True(t, resource.EqualType(res.Owner.Type, ServiceType))
require.True(t, resource.EqualTenancy(res.Owner.Tenancy, defaultEndpointTenancy))
require.Equal(t, res.Owner.Name, res.Id.Name)
}

View File

@ -4,7 +4,6 @@
package resource package resource
import ( import (
"errors"
"fmt" "fmt"
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
@ -12,11 +11,33 @@ import (
) )
var ( var (
ErrMissing = errors.New("missing required field") ErrMissing = NewConstError("missing required field")
ErrEmpty = errors.New("cannot be empty") ErrEmpty = NewConstError("cannot be empty")
ErrReferenceTenancyNotEqual = errors.New("resource tenancy and reference tenancy differ") ErrReferenceTenancyNotEqual = NewConstError("resource tenancy and reference tenancy differ")
) )
// ConstError is more or less equivalent to the stdlib errors.errorstring. However, having
// our own exported type allows us to more accurately compare error values in tests.
//
// - go-cmp will not compared unexported fields by default.
// - cmp.AllowUnexported(<type>) requires a concrete struct type and due to the stdlib not
// exporting the errorstring type there doesn't seem to be a way to get at the type.
// - cmpopts.EquateErrors has issues with protobuf types within other error structs.
//
// Due to these factors the easiest thing to do is to create a custom comparer for
// the ConstError type and use it where necessary.
type ConstError struct {
message string
}
func NewConstError(msg string) ConstError {
return ConstError{message: msg}
}
func (e ConstError) Error() string {
return e.message
}
type ErrDataParse struct { type ErrDataParse struct {
TypeName string TypeName string
Wrapped error Wrapped error

View File

@ -55,6 +55,11 @@ func ResourceID(id *pbresource.ID) *resourceBuilder {
} }
} }
func (b *resourceBuilder) WithTenancy(tenant *pbresource.Tenancy) *resourceBuilder {
b.resource.Id.Tenancy = tenant
return b
}
func (b *resourceBuilder) WithData(t T, data protoreflect.ProtoMessage) *resourceBuilder { func (b *resourceBuilder) WithData(t T, data protoreflect.ProtoMessage) *resourceBuilder {
t.Helper() t.Helper()

View File

@ -2,12 +2,38 @@ package resourcetest
import ( import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/proto/private/prototest" "github.com/hashicorp/consul/proto/private/prototest"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/testing/protocmp"
) )
// CompareErrorString is a helper to generate a custom go-cmp comparer method
// that will perform an equality check on the error message. This is mainly
// useful to get around not being able to see unexported data within errors.
func CompareErrorString[T error]() cmp.Option {
return cmp.Comparer(func(e1, e2 T) bool {
return e1.Error() == e2.Error()
})
}
// default comparers for known types that don't play well with go-cmp
var comparers = []cmp.Option{
CompareErrorString[resource.ConstError](),
}
// RequireError is useful for asserting that some chained multierror contains a specific error.
func RequireError[E error](t T, err error, expected E, opts ...cmp.Option) {
t.Helper()
var actual E
require.ErrorAs(t, err, &actual)
opts = append(opts, comparers...)
prototest.AssertDeepEqual(t, expected, actual, opts...)
}
func RequireVersionUnchanged(t T, res *pbresource.Resource, version string) { func RequireVersionUnchanged(t T, res *pbresource.Resource, version string) {
t.Helper() t.Helper()
require.Equal(t, version, res.Version) require.Equal(t, version, res.Version)