mirror of
https://github.com/status-im/consul.git
synced 2025-02-09 04:14:50 +00:00
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:
parent
43d8898e08
commit
91d331bbaa
@ -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{
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user