Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ServiceEndpoints Mutation hook tests #18404

Merged
merged 2 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions internal/catalog/internal/types/service_endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
if !resource.EqualType(res.Owner.Type, ServiceV1Alpha1Type) {
err = multierror.Append(err, resource.ErrOwnerTypeInvalid{
Expand All @@ -54,6 +64,7 @@ func MutateServiceEndpoints(res *pbresource.Resource) error {

if !resource.EqualTenancy(res.Owner.Tenancy, res.Id.Tenancy) {
err = multierror.Append(err, resource.ErrOwnerTenantInvalid{
ResourceType: ServiceEndpointsV1Alpha1Type,
ResourceTenancy: res.Id.Tenancy,
OwnerTenancy: res.Owner.Tenancy,
})
Expand All @@ -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 {
if endpointErr := validateEndpoint(endpoint, res); endpointErr != nil {
err = multierror.Append(err, resource.ErrInvalidListElement{
Expand Down
95 changes: 67 additions & 28 deletions internal/catalog/internal/types/service_endpoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import (
"testing"

"github.com/hashicorp/consul/internal/resource"
rtest "github.com/hashicorp/consul/internal/resource/resourcetest"
pbcatalog "github.com/hashicorp/consul/proto-public/pbcatalog/v1alpha1"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/anypb"
)

var (
Expand All @@ -20,22 +19,13 @@ var (
Namespace: "default",
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",
},
badEndpointTenancy = &pbresource.Tenancy{
Partition: "default",
Namespace: "bad",
PeerName: "local",
}

var err error
res.Data, err = anypb.New(data)
require.NoError(t, err)
return res
}
)

func TestValidateServiceEndpoints_Ok(t *testing.T) {
data := &pbcatalog.ServiceEndpoints{
Expand All @@ -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)
require.NoError(t, err)
}
Expand All @@ -73,7 +69,7 @@ func TestValidateServiceEndpoints_ParseError(t *testing.T) {
// to cause the error we are expecting
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)
require.Error(t, err)
Expand Down Expand Up @@ -104,6 +100,7 @@ func TestValidateServiceEndpoints_EndpointInvalid(t *testing.T) {
}

type testCase struct {
owner *pbresource.ID
modify func(*pbcatalog.Endpoint)
validateErr func(t *testing.T, err error)
}
Expand Down Expand Up @@ -140,11 +137,11 @@ func TestValidateServiceEndpoints_EndpointInvalid(t *testing.T) {
}
},
validateErr: func(t *testing.T, err error) {
var mapErr resource.ErrInvalidMapKey
require.ErrorAs(t, err, &mapErr)
require.Equal(t, "ports", mapErr.Map)
require.Equal(t, "", mapErr.Key)
require.Equal(t, resource.ErrEmpty, mapErr.Wrapped)
rtest.RequireError(t, err, resource.ErrInvalidMapKey{
Map: "ports",
Key: "",
Wrapped: resource.ErrEmpty,
})
},
},
"port-0": {
Expand All @@ -163,22 +160,64 @@ func TestValidateServiceEndpoints_EndpointInvalid(t *testing.T) {
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 {
t.Run(name, func(t *testing.T) {
data := genData()
tcase.modify(data)
endpoint := genData()
if tcase.modify != nil {
tcase.modify(endpoint)
}

res := createServiceEndpointsResource(t, &pbcatalog.ServiceEndpoints{
data := &pbcatalog.ServiceEndpoints{
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)
require.Error(t, err)
tcase.validateErr(t, err)
})
}
}

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)
}
29 changes: 25 additions & 4 deletions internal/resource/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,40 @@
package resource

import (
"errors"
"fmt"

"github.com/hashicorp/consul/proto-public/pbresource"
"google.golang.org/protobuf/reflect/protoreflect"
)

var (
ErrMissing = errors.New("missing required field")
ErrEmpty = errors.New("cannot be empty")
ErrReferenceTenancyNotEqual = errors.New("resource tenancy and reference tenancy differ")
ErrMissing = NewConstError("missing required field")
ErrEmpty = NewConstError("cannot be empty")
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 {
TypeName string
Wrapped error
Expand Down
5 changes: 5 additions & 0 deletions internal/resource/resourcetest/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
t.Helper()

Expand Down
26 changes: 26 additions & 0 deletions internal/resource/resourcetest/require.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,38 @@ package resourcetest

import (
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/proto/private/prototest"
"github.com/stretchr/testify/require"
"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) {
t.Helper()
require.Equal(t, version, res.Version)
Expand Down