mirror of https://github.com/hashicorp/consul
Semir Patel
10 months ago
committed by
GitHub
17 changed files with 1251 additions and 511 deletions
@ -0,0 +1,139 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package resource |
||||
|
||||
import ( |
||||
"context" |
||||
"strings" |
||||
|
||||
"google.golang.org/grpc/codes" |
||||
"google.golang.org/grpc/status" |
||||
|
||||
"github.com/hashicorp/consul/acl" |
||||
"github.com/hashicorp/consul/internal/resource" |
||||
"github.com/hashicorp/consul/proto-public/pbresource" |
||||
) |
||||
|
||||
func (s *Server) MutateAndValidate(ctx context.Context, req *pbresource.MutateAndValidateRequest) (*pbresource.MutateAndValidateResponse, error) { |
||||
tenancyMarkedForDeletion, err := s.mutateAndValidate(ctx, req.Resource) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if tenancyMarkedForDeletion { |
||||
return nil, status.Errorf( |
||||
codes.InvalidArgument, |
||||
"tenancy marked for deletion: %s/%s", |
||||
req.Resource.Id.Tenancy.Partition, |
||||
req.Resource.Id.Tenancy.Namespace, |
||||
) |
||||
} |
||||
return &pbresource.MutateAndValidateResponse{Resource: req.Resource}, nil |
||||
} |
||||
|
||||
// private DRY impl that is used by both the Write and MutateAndValidate RPCs.
|
||||
func (s *Server) mutateAndValidate(ctx context.Context, res *pbresource.Resource) (tenancyMarkedForDeletion bool, err error) { |
||||
reg, err := s.ensureResourceValid(res) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
v1EntMeta := v2TenancyToV1EntMeta(res.Id.Tenancy) |
||||
authz, authzContext, err := s.getAuthorizer(tokenFromContext(ctx), v1EntMeta) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
v1EntMetaToV2Tenancy(reg, v1EntMeta, res.Id.Tenancy) |
||||
|
||||
// Check the user sent the correct type of data.
|
||||
if res.Data != nil && !res.Data.MessageIs(reg.Proto) { |
||||
got := strings.TrimPrefix(res.Data.TypeUrl, "type.googleapis.com/") |
||||
|
||||
return false, status.Errorf( |
||||
codes.InvalidArgument, |
||||
"resource.data is of wrong type (expected=%q, got=%q)", |
||||
reg.Proto.ProtoReflect().Descriptor().FullName(), |
||||
got, |
||||
) |
||||
} |
||||
|
||||
if err = reg.Mutate(res); err != nil { |
||||
return false, status.Errorf(codes.Internal, "failed mutate hook: %v", err.Error()) |
||||
} |
||||
|
||||
if err = reg.Validate(res); err != nil { |
||||
return false, status.Error(codes.InvalidArgument, err.Error()) |
||||
} |
||||
|
||||
// ACL check comes before tenancy existence checks to not leak tenancy "existence".
|
||||
err = reg.ACLs.Write(authz, authzContext, res) |
||||
switch { |
||||
case acl.IsErrPermissionDenied(err): |
||||
return false, status.Error(codes.PermissionDenied, err.Error()) |
||||
case err != nil: |
||||
return false, status.Errorf(codes.Internal, "failed write acl: %v", err) |
||||
} |
||||
|
||||
// Check tenancy exists for the V2 resource
|
||||
if err = tenancyExists(reg, s.TenancyBridge, res.Id.Tenancy, codes.InvalidArgument); err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
// This is used later in the "create" and "update" paths to block non-delete related writes
|
||||
// when a tenancy unit has been marked for deletion.
|
||||
tenancyMarkedForDeletion, err = isTenancyMarkedForDeletion(reg, s.TenancyBridge, res.Id.Tenancy) |
||||
if err != nil { |
||||
return false, status.Errorf(codes.Internal, "failed tenancy marked for deletion check: %v", err) |
||||
} |
||||
if tenancyMarkedForDeletion { |
||||
return true, nil |
||||
} |
||||
return false, nil |
||||
} |
||||
|
||||
func (s *Server) ensureResourceValid(res *pbresource.Resource) (*resource.Registration, error) { |
||||
var field string |
||||
switch { |
||||
case res == nil: |
||||
field = "resource" |
||||
case res.Id == nil: |
||||
field = "resource.id" |
||||
} |
||||
|
||||
if field != "" { |
||||
return nil, status.Errorf(codes.InvalidArgument, "%s is required", field) |
||||
} |
||||
|
||||
if err := validateId(res.Id, "resource.id"); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if res.Owner != nil { |
||||
if err := validateId(res.Owner, "resource.owner"); err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
// Check type exists.
|
||||
reg, err := s.resolveType(res.Id.Type) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if err = checkV2Tenancy(s.UseV2Tenancy, res.Id.Type); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// Check scope
|
||||
if reg.Scope == resource.ScopePartition && res.Id.Tenancy.Namespace != "" { |
||||
return nil, status.Errorf( |
||||
codes.InvalidArgument, |
||||
"partition scoped resource %s cannot have a namespace. got: %s", |
||||
resource.ToGVK(res.Id.Type), |
||||
res.Id.Tenancy.Namespace, |
||||
) |
||||
} |
||||
|
||||
return reg, nil |
||||
} |
@ -0,0 +1,212 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package resource_test |
||||
|
||||
import ( |
||||
"fmt" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
"google.golang.org/grpc/codes" |
||||
"google.golang.org/grpc/status" |
||||
|
||||
svc "github.com/hashicorp/consul/agent/grpc-external/services/resource" |
||||
svctest "github.com/hashicorp/consul/agent/grpc-external/services/resource/testing" |
||||
"github.com/hashicorp/consul/internal/resource/demo" |
||||
"github.com/hashicorp/consul/proto-public/pbresource" |
||||
pbdemov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2" |
||||
"github.com/hashicorp/consul/proto/private/prototest" |
||||
) |
||||
|
||||
func TestMutateAndValidate_InputValidation(t *testing.T) { |
||||
run := func(t *testing.T, client pbresource.ResourceServiceClient, tc resourceValidTestCase) { |
||||
artist, err := demo.GenerateV2Artist() |
||||
require.NoError(t, err) |
||||
|
||||
recordLabel, err := demo.GenerateV1RecordLabel("looney-tunes") |
||||
require.NoError(t, err) |
||||
|
||||
req := &pbresource.MutateAndValidateRequest{Resource: tc.modFn(artist, recordLabel)} |
||||
_, err = client.MutateAndValidate(testContext(t), req) |
||||
require.Error(t, err) |
||||
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) |
||||
require.ErrorContains(t, err, tc.errContains) |
||||
} |
||||
|
||||
for _, v2tenancy := range []bool{false, true} { |
||||
t.Run(fmt.Sprintf("v2tenancy %v", v2tenancy), func(t *testing.T) { |
||||
client := svctest.NewResourceServiceBuilder(). |
||||
WithRegisterFns(demo.RegisterTypes). |
||||
WithV2Tenancy(v2tenancy). |
||||
Run(t) |
||||
|
||||
for desc, tc := range resourceValidTestCases(t) { |
||||
t.Run(desc, func(t *testing.T) { |
||||
run(t, client, tc) |
||||
}) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestMutateAndValidate_OwnerValidation(t *testing.T) { |
||||
run := func(t *testing.T, client pbresource.ResourceServiceClient, tc ownerValidTestCase) { |
||||
artist, err := demo.GenerateV2Artist() |
||||
require.NoError(t, err) |
||||
|
||||
album, err := demo.GenerateV2Album(artist.Id) |
||||
require.NoError(t, err) |
||||
|
||||
tc.modFn(album) |
||||
|
||||
_, err = client.MutateAndValidate(testContext(t), &pbresource.MutateAndValidateRequest{Resource: album}) |
||||
require.Error(t, err) |
||||
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) |
||||
require.ErrorContains(t, err, tc.errorContains) |
||||
} |
||||
|
||||
for _, v2tenancy := range []bool{false, true} { |
||||
t.Run(fmt.Sprintf("v2tenancy %v", v2tenancy), func(t *testing.T) { |
||||
client := svctest.NewResourceServiceBuilder(). |
||||
WithRegisterFns(demo.RegisterTypes). |
||||
WithV2Tenancy(v2tenancy). |
||||
Run(t) |
||||
|
||||
for desc, tc := range ownerValidationTestCases(t) { |
||||
t.Run(desc, func(t *testing.T) { |
||||
run(t, client, tc) |
||||
}) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestMutateAndValidate_TypeNotFound(t *testing.T) { |
||||
run := func(t *testing.T, client pbresource.ResourceServiceClient) { |
||||
res, err := demo.GenerateV2Artist() |
||||
require.NoError(t, err) |
||||
|
||||
_, err = client.MutateAndValidate(testContext(t), &pbresource.MutateAndValidateRequest{Resource: res}) |
||||
require.Error(t, err) |
||||
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) |
||||
require.Contains(t, err.Error(), "resource type demo.v2.Artist not registered") |
||||
} |
||||
|
||||
for _, v2tenancy := range []bool{false, true} { |
||||
t.Run(fmt.Sprintf("v2tenancy %v", v2tenancy), func(t *testing.T) { |
||||
client := svctest.NewResourceServiceBuilder().WithV2Tenancy(v2tenancy).Run(t) |
||||
run(t, client) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestMutateAndValidate_Success(t *testing.T) { |
||||
run := func(t *testing.T, client pbresource.ResourceServiceClient, tc mavOrWriteSuccessTestCase) { |
||||
recordLabel, err := demo.GenerateV1RecordLabel("looney-tunes") |
||||
require.NoError(t, err) |
||||
|
||||
artist, err := demo.GenerateV2Artist() |
||||
require.NoError(t, err) |
||||
|
||||
rsp, err := client.MutateAndValidate(testContext(t), &pbresource.MutateAndValidateRequest{Resource: tc.modFn(artist, recordLabel)}) |
||||
require.NoError(t, err) |
||||
prototest.AssertDeepEqual(t, tc.expectedTenancy, rsp.Resource.Id.Tenancy) |
||||
} |
||||
|
||||
for _, v2tenancy := range []bool{false, true} { |
||||
t.Run(fmt.Sprintf("v2tenancy %v", v2tenancy), func(t *testing.T) { |
||||
client := svctest.NewResourceServiceBuilder(). |
||||
WithRegisterFns(demo.RegisterTypes). |
||||
WithV2Tenancy(v2tenancy). |
||||
Run(t) |
||||
|
||||
for desc, tc := range mavOrWriteSuccessTestCases(t) { |
||||
t.Run(desc, func(t *testing.T) { |
||||
run(t, client, tc) |
||||
}) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestMutateAndValidate_Mutate(t *testing.T) { |
||||
for _, v2tenancy := range []bool{false, true} { |
||||
t.Run(fmt.Sprintf("v2tenancy %v", v2tenancy), func(t *testing.T) { |
||||
client := svctest.NewResourceServiceBuilder(). |
||||
WithRegisterFns(demo.RegisterTypes). |
||||
WithV2Tenancy(v2tenancy). |
||||
Run(t) |
||||
|
||||
artist, err := demo.GenerateV2Artist() |
||||
require.NoError(t, err) |
||||
|
||||
artistData := &pbdemov2.Artist{} |
||||
artist.Data.UnmarshalTo(artistData) |
||||
require.NoError(t, err) |
||||
|
||||
// mutate hook sets genre to disco when unspecified
|
||||
artistData.Genre = pbdemov2.Genre_GENRE_UNSPECIFIED |
||||
artist.Data.MarshalFrom(artistData) |
||||
require.NoError(t, err) |
||||
|
||||
rsp, err := client.MutateAndValidate(testContext(t), &pbresource.MutateAndValidateRequest{Resource: artist}) |
||||
require.NoError(t, err) |
||||
|
||||
// verify mutate hook set genre to disco
|
||||
require.NoError(t, rsp.Resource.Data.UnmarshalTo(artistData)) |
||||
require.Equal(t, pbdemov2.Genre_GENRE_DISCO, artistData.Genre) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestMutateAndValidate_Tenancy_NotFound(t *testing.T) { |
||||
for desc, tc := range mavOrWriteTenancyNotFoundTestCases(t) { |
||||
t.Run(desc, func(t *testing.T) { |
||||
client := svctest.NewResourceServiceBuilder(). |
||||
WithV2Tenancy(true). |
||||
WithRegisterFns(demo.RegisterTypes). |
||||
Run(t) |
||||
|
||||
recordLabel, err := demo.GenerateV1RecordLabel("looney-tunes") |
||||
require.NoError(t, err) |
||||
|
||||
artist, err := demo.GenerateV2Artist() |
||||
require.NoError(t, err) |
||||
|
||||
_, err = client.MutateAndValidate(testContext(t), &pbresource.MutateAndValidateRequest{Resource: tc.modFn(artist, recordLabel)}) |
||||
require.Error(t, err) |
||||
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) |
||||
require.Contains(t, err.Error(), tc.errContains) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestMutateAndValidate_TenancyMarkedForDeletion_Fails(t *testing.T) { |
||||
for desc, tc := range mavOrWriteTenancyMarkedForDeletionTestCases(t) { |
||||
t.Run(desc, func(t *testing.T) { |
||||
server := testServer(t) |
||||
client := testClient(t, server) |
||||
demo.RegisterTypes(server.Registry) |
||||
|
||||
recordLabel, err := demo.GenerateV1RecordLabel("looney-tunes") |
||||
require.NoError(t, err) |
||||
recordLabel.Id.Tenancy.Partition = "ap1" |
||||
|
||||
artist, err := demo.GenerateV2Artist() |
||||
require.NoError(t, err) |
||||
artist.Id.Tenancy.Partition = "ap1" |
||||
artist.Id.Tenancy.Namespace = "ns1" |
||||
|
||||
mockTenancyBridge := &svc.MockTenancyBridge{} |
||||
mockTenancyBridge.On("PartitionExists", "ap1").Return(true, nil) |
||||
mockTenancyBridge.On("NamespaceExists", "ap1", "ns1").Return(true, nil) |
||||
server.TenancyBridge = mockTenancyBridge |
||||
|
||||
_, err = client.MutateAndValidate(testContext(t), &pbresource.MutateAndValidateRequest{Resource: tc.modFn(artist, recordLabel, mockTenancyBridge)}) |
||||
require.Error(t, err) |
||||
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) |
||||
require.Contains(t, err.Error(), tc.errContains) |
||||
}) |
||||
} |
||||
} |
@ -0,0 +1,328 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package resource_test |
||||
|
||||
import ( |
||||
"strings" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
"google.golang.org/grpc/codes" |
||||
"google.golang.org/protobuf/types/known/anypb" |
||||
|
||||
svc "github.com/hashicorp/consul/agent/grpc-external/services/resource" |
||||
"github.com/hashicorp/consul/internal/resource" |
||||
"github.com/hashicorp/consul/proto-public/pbresource" |
||||
pbdemov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2" |
||||
) |
||||
|
||||
// Common test structs and test cases shared by the Write and MutateAndValidate RPCs
|
||||
// only. These are not intended to be used by other tests.
|
||||
|
||||
type resourceValidTestCase struct { |
||||
modFn func(artist, recordLabel *pbresource.Resource) *pbresource.Resource |
||||
errContains string |
||||
} |
||||
|
||||
func resourceValidTestCases(t *testing.T) map[string]resourceValidTestCase { |
||||
return map[string]resourceValidTestCase{ |
||||
"no resource": { |
||||
modFn: func(_, _ *pbresource.Resource) *pbresource.Resource { |
||||
return nil |
||||
}, |
||||
errContains: "resource is required", |
||||
}, |
||||
"no id": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id = nil |
||||
return artist |
||||
}, |
||||
errContains: "resource.id is required", |
||||
}, |
||||
"no type": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Type = nil |
||||
return artist |
||||
}, |
||||
errContains: "resource.id.type is required", |
||||
}, |
||||
"no name": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Name = "" |
||||
return artist |
||||
}, |
||||
errContains: "resource.id.name invalid", |
||||
}, |
||||
"name is mixed case": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Name = "MixedCaseNotAllowed" |
||||
return artist |
||||
}, |
||||
errContains: "resource.id.name invalid", |
||||
}, |
||||
"name too long": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Name = strings.Repeat("a", resource.MaxNameLength+1) |
||||
return artist |
||||
}, |
||||
errContains: "resource.id.name invalid", |
||||
}, |
||||
"wrong data type": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
var err error |
||||
artist.Data, err = anypb.New(&pbdemov2.Album{}) |
||||
require.NoError(t, err) |
||||
return artist |
||||
}, |
||||
errContains: "resource.data is of wrong type", |
||||
}, |
||||
"partition is mixed case": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Tenancy.Partition = "Default" |
||||
return artist |
||||
}, |
||||
errContains: "resource.id.tenancy.partition invalid", |
||||
}, |
||||
"partition too long": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Tenancy.Partition = strings.Repeat("p", resource.MaxNameLength+1) |
||||
return artist |
||||
}, |
||||
errContains: "resource.id.tenancy.partition invalid", |
||||
}, |
||||
"namespace is mixed case": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Tenancy.Namespace = "Default" |
||||
return artist |
||||
}, |
||||
errContains: "resource.id.tenancy.namespace invalid", |
||||
}, |
||||
"namespace too long": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Tenancy.Namespace = strings.Repeat("n", resource.MaxNameLength+1) |
||||
return artist |
||||
}, |
||||
errContains: "resource.id.tenancy.namespace invalid", |
||||
}, |
||||
"fail validation hook": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
buffer := &pbdemov2.Artist{} |
||||
require.NoError(t, artist.Data.UnmarshalTo(buffer)) |
||||
buffer.Name = "" // name cannot be empty
|
||||
require.NoError(t, artist.Data.MarshalFrom(buffer)) |
||||
return artist |
||||
}, |
||||
errContains: "artist.name required", |
||||
}, |
||||
"partition scope with non-empty namespace": { |
||||
modFn: func(_, recordLabel *pbresource.Resource) *pbresource.Resource { |
||||
recordLabel.Id.Tenancy.Namespace = "bogus" |
||||
return recordLabel |
||||
}, |
||||
errContains: "cannot have a namespace", |
||||
}, |
||||
} |
||||
} |
||||
|
||||
type ownerValidTestCase struct { |
||||
modFn func(res *pbresource.Resource) |
||||
errorContains string |
||||
} |
||||
|
||||
func ownerValidationTestCases(t *testing.T) map[string]ownerValidTestCase { |
||||
return map[string]ownerValidTestCase{ |
||||
"no owner type": { |
||||
modFn: func(res *pbresource.Resource) { res.Owner.Type = nil }, |
||||
errorContains: "resource.owner.type is required", |
||||
}, |
||||
"no owner name": { |
||||
modFn: func(res *pbresource.Resource) { res.Owner.Name = "" }, |
||||
errorContains: "resource.owner.name invalid", |
||||
}, |
||||
"mixed case owner name": { |
||||
modFn: func(res *pbresource.Resource) { res.Owner.Name = strings.ToUpper(res.Owner.Name) }, |
||||
errorContains: "resource.owner.name invalid", |
||||
}, |
||||
"owner name too long": { |
||||
modFn: func(res *pbresource.Resource) { |
||||
res.Owner.Name = strings.Repeat("a", resource.MaxNameLength+1) |
||||
}, |
||||
errorContains: "resource.owner.name invalid", |
||||
}, |
||||
"owner partition is mixed case": { |
||||
modFn: func(res *pbresource.Resource) { |
||||
res.Owner.Tenancy.Partition = "Default" |
||||
}, |
||||
errorContains: "resource.owner.tenancy.partition invalid", |
||||
}, |
||||
"owner partition too long": { |
||||
modFn: func(res *pbresource.Resource) { |
||||
res.Owner.Tenancy.Partition = strings.Repeat("p", resource.MaxNameLength+1) |
||||
}, |
||||
errorContains: "resource.owner.tenancy.partition invalid", |
||||
}, |
||||
"owner namespace is mixed case": { |
||||
modFn: func(res *pbresource.Resource) { |
||||
res.Owner.Tenancy.Namespace = "Default" |
||||
}, |
||||
errorContains: "resource.owner.tenancy.namespace invalid", |
||||
}, |
||||
"owner namespace too long": { |
||||
modFn: func(res *pbresource.Resource) { |
||||
res.Owner.Tenancy.Namespace = strings.Repeat("n", resource.MaxNameLength+1) |
||||
}, |
||||
errorContains: "resource.owner.tenancy.namespace invalid", |
||||
}, |
||||
} |
||||
} |
||||
|
||||
// Test case struct shared by MutateAndValidate and Write success test cases
|
||||
type mavOrWriteSuccessTestCase struct { |
||||
modFn func(artist, recordLabel *pbresource.Resource) *pbresource.Resource |
||||
expectedTenancy *pbresource.Tenancy |
||||
} |
||||
|
||||
// Test case struct shared by MutateAndValidate and Write success test cases
|
||||
func mavOrWriteSuccessTestCases(t *testing.T) map[string]mavOrWriteSuccessTestCase { |
||||
return map[string]mavOrWriteSuccessTestCase{ |
||||
"namespaced resource provides nonempty partition and namespace": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
return artist |
||||
}, |
||||
expectedTenancy: resource.DefaultNamespacedTenancy(), |
||||
}, |
||||
"namespaced resource inherits tokens partition when empty": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Tenancy.Partition = "" |
||||
return artist |
||||
}, |
||||
expectedTenancy: resource.DefaultNamespacedTenancy(), |
||||
}, |
||||
"namespaced resource inherits tokens namespace when empty": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Tenancy.Namespace = "" |
||||
return artist |
||||
}, |
||||
expectedTenancy: resource.DefaultNamespacedTenancy(), |
||||
}, |
||||
"namespaced resource inherits tokens partition and namespace when empty": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Tenancy.Partition = "" |
||||
artist.Id.Tenancy.Namespace = "" |
||||
return artist |
||||
}, |
||||
expectedTenancy: resource.DefaultNamespacedTenancy(), |
||||
}, |
||||
"namespaced resource inherits tokens partition and namespace when tenancy nil": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Tenancy = nil |
||||
return artist |
||||
}, |
||||
expectedTenancy: resource.DefaultNamespacedTenancy(), |
||||
}, |
||||
"namespaced resource defaults peername to local when empty": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Tenancy.PeerName = "" |
||||
return artist |
||||
}, |
||||
expectedTenancy: resource.DefaultNamespacedTenancy(), |
||||
}, |
||||
"partitioned resource provides nonempty partition": { |
||||
modFn: func(_, recordLabel *pbresource.Resource) *pbresource.Resource { |
||||
return recordLabel |
||||
}, |
||||
expectedTenancy: resource.DefaultPartitionedTenancy(), |
||||
}, |
||||
"partitioned resource inherits tokens partition when empty": { |
||||
modFn: func(_, recordLabel *pbresource.Resource) *pbresource.Resource { |
||||
recordLabel.Id.Tenancy.Partition = "" |
||||
return recordLabel |
||||
}, |
||||
expectedTenancy: resource.DefaultPartitionedTenancy(), |
||||
}, |
||||
"partitioned resource inherits tokens partition when tenancy nil": { |
||||
modFn: func(_, recordLabel *pbresource.Resource) *pbresource.Resource { |
||||
recordLabel.Id.Tenancy = nil |
||||
return recordLabel |
||||
}, |
||||
expectedTenancy: resource.DefaultPartitionedTenancy(), |
||||
}, |
||||
"partitioned resource defaults peername to local when empty": { |
||||
modFn: func(_, recordLabel *pbresource.Resource) *pbresource.Resource { |
||||
recordLabel.Id.Tenancy.PeerName = "" |
||||
return recordLabel |
||||
}, |
||||
expectedTenancy: resource.DefaultPartitionedTenancy(), |
||||
}, |
||||
} |
||||
} |
||||
|
||||
// Test case struct shared by MutateAndValidate and Write test cases where tenancy is not found
|
||||
type mavOrWriteTenancyNotFoundTestCase map[string]struct { |
||||
modFn func(artist, recordLabel *pbresource.Resource) *pbresource.Resource |
||||
errCode codes.Code |
||||
errContains string |
||||
} |
||||
|
||||
// Test case struct shared by MutateAndValidate and Write test cases where tenancy is not found
|
||||
func mavOrWriteTenancyNotFoundTestCases(t *testing.T) mavOrWriteTenancyNotFoundTestCase { |
||||
return mavOrWriteTenancyNotFoundTestCase{ |
||||
"namespaced resource provides nonexistant partition": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Tenancy.Partition = "boguspartition" |
||||
return artist |
||||
}, |
||||
errCode: codes.InvalidArgument, |
||||
errContains: "partition not found", |
||||
}, |
||||
"namespaced resource provides nonexistant namespace": { |
||||
modFn: func(artist, _ *pbresource.Resource) *pbresource.Resource { |
||||
artist.Id.Tenancy.Namespace = "bogusnamespace" |
||||
return artist |
||||
}, |
||||
errCode: codes.InvalidArgument, |
||||
errContains: "namespace not found", |
||||
}, |
||||
"partitioned resource provides nonexistant partition": { |
||||
modFn: func(_, recordLabel *pbresource.Resource) *pbresource.Resource { |
||||
recordLabel.Id.Tenancy.Partition = "boguspartition" |
||||
return recordLabel |
||||
}, |
||||
errCode: codes.InvalidArgument, |
||||
errContains: "partition not found", |
||||
}, |
||||
} |
||||
} |
||||
|
||||
type mavOrWriteTenancyMarkedForDeletionTestCase struct { |
||||
modFn func(artist, recordLabel *pbresource.Resource, mockTenancyBridge *svc.MockTenancyBridge) *pbresource.Resource |
||||
errContains string |
||||
} |
||||
|
||||
func mavOrWriteTenancyMarkedForDeletionTestCases(t *testing.T) map[string]mavOrWriteTenancyMarkedForDeletionTestCase { |
||||
return map[string]mavOrWriteTenancyMarkedForDeletionTestCase{ |
||||
"namespaced resources partition marked for deletion": { |
||||
modFn: func(artist, _ *pbresource.Resource, mockTenancyBridge *svc.MockTenancyBridge) *pbresource.Resource { |
||||
mockTenancyBridge.On("IsPartitionMarkedForDeletion", "ap1").Return(true, nil) |
||||
return artist |
||||
}, |
||||
errContains: "tenancy marked for deletion", |
||||
}, |
||||
"namespaced resources namespace marked for deletion": { |
||||
modFn: func(artist, _ *pbresource.Resource, mockTenancyBridge *svc.MockTenancyBridge) *pbresource.Resource { |
||||
mockTenancyBridge.On("IsPartitionMarkedForDeletion", "ap1").Return(false, nil) |
||||
mockTenancyBridge.On("IsNamespaceMarkedForDeletion", "ap1", "ns1").Return(true, nil) |
||||
return artist |
||||
}, |
||||
errContains: "tenancy marked for deletion", |
||||
}, |
||||
"partitioned resources partition marked for deletion": { |
||||
modFn: func(_, recordLabel *pbresource.Resource, mockTenancyBridge *svc.MockTenancyBridge) *pbresource.Resource { |
||||
mockTenancyBridge.On("IsPartitionMarkedForDeletion", "ap1").Return(true, nil) |
||||
return recordLabel |
||||
}, |
||||
errContains: "tenancy marked for deletion", |
||||
}, |
||||
} |
||||
} |
Loading…
Reference in new issue