diff --git a/sdk/sdk.go b/sdk/sdk.go index de82bc07a1..16f3102be0 100644 --- a/sdk/sdk.go +++ b/sdk/sdk.go @@ -22,6 +22,7 @@ import ( "github.com/opentdf/platform/protocol/go/policy/attributes" "github.com/opentdf/platform/protocol/go/policy/kasregistry" "github.com/opentdf/platform/protocol/go/policy/namespaces" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" "github.com/opentdf/platform/protocol/go/policy/resourcemapping" "github.com/opentdf/platform/protocol/go/policy/subjectmapping" "github.com/opentdf/platform/protocol/go/policy/unsafe" @@ -70,6 +71,7 @@ type SDK struct { EntityResoution entityresolution.EntityResolutionServiceClient KeyAccessServerRegistry kasregistry.KeyAccessServerRegistryServiceClient Namespaces namespaces.NamespaceServiceClient + RegisteredResources registeredresources.RegisteredResourcesServiceClient ResourceMapping resourcemapping.ResourceMappingServiceClient SubjectMapping subjectmapping.SubjectMappingServiceClient Unsafe unsafe.UnsafeServiceClient @@ -198,6 +200,7 @@ func New(platformEndpoint string, opts ...Option) (*SDK, error) { Actions: actions.NewActionServiceClient(platformConn), Attributes: attributes.NewAttributesServiceClient(platformConn), Namespaces: namespaces.NewNamespaceServiceClient(platformConn), + RegisteredResources: registeredresources.NewRegisteredResourcesServiceClient(platformConn), ResourceMapping: resourcemapping.NewResourceMappingServiceClient(platformConn), SubjectMapping: subjectmapping.NewSubjectMappingServiceClient(platformConn), Unsafe: unsafe.NewUnsafeServiceClient(platformConn), diff --git a/service/logger/audit/constants.go b/service/logger/audit/constants.go index 076166f569..9808cf0fdc 100644 --- a/service/logger/audit/constants.go +++ b/service/logger/audit/constants.go @@ -22,6 +22,8 @@ const ( ObjectTypeResourceMappingGroup ObjectTypePublicKey ObjectTypeAction + ObjectTypeRegisteredResource + ObjectTypeRegisteredResourceValue ) func (ot ObjectType) String() string { @@ -41,6 +43,8 @@ func (ot ObjectType) String() string { "resource_mapping_group", "public_key", "action", + "registered_resource", + "registered_resource_value", }[ot] } diff --git a/service/pkg/server/services_test.go b/service/pkg/server/services_test.go index 00d2e0c2cd..320eca748c 100644 --- a/service/pkg/server/services_test.go +++ b/service/pkg/server/services_test.go @@ -26,7 +26,7 @@ type mockTestServiceOptions struct { dbRegister serviceregistry.DBRegister } -const numExpectedPolicyServices = 7 +const numExpectedPolicyServices = 8 func mockTestServiceRegistry(opts mockTestServiceOptions) (serviceregistry.IService, *spyTestService) { spy := &spyTestService{} diff --git a/service/policy/policy.go b/service/policy/policy.go index 99d1e437bc..1fcbc3ede1 100644 --- a/service/policy/policy.go +++ b/service/policy/policy.go @@ -9,6 +9,7 @@ import ( "github.com/opentdf/platform/service/policy/db/migrations" "github.com/opentdf/platform/service/policy/kasregistry" "github.com/opentdf/platform/service/policy/namespaces" + "github.com/opentdf/platform/service/policy/registeredresources" "github.com/opentdf/platform/service/policy/resourcemapping" "github.com/opentdf/platform/service/policy/subjectmapping" "github.com/opentdf/platform/service/policy/unsafe" @@ -36,6 +37,7 @@ func NewRegistrations() []serviceregistry.IService { kasregistry.NewRegistration(namespace, dbRegister), unsafe.NewRegistration(namespace, dbRegister), actions.NewRegistration(namespace, dbRegister), + registeredresources.NewRegistration(namespace, dbRegister), }...) return registrations } diff --git a/service/policy/registeredresources/registered_resources.go b/service/policy/registeredresources/registered_resources.go new file mode 100644 index 0000000000..142b7bb236 --- /dev/null +++ b/service/policy/registeredresources/registered_resources.go @@ -0,0 +1,335 @@ +package registeredresources + +import ( + "context" + "fmt" + "log/slog" + + "connectrpc.com/connect" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/opentdf/platform/protocol/go/policy/registeredresources/registeredresourcesconnect" + "github.com/opentdf/platform/service/logger" + "github.com/opentdf/platform/service/logger/audit" + "github.com/opentdf/platform/service/pkg/config" + "github.com/opentdf/platform/service/pkg/db" + "github.com/opentdf/platform/service/pkg/serviceregistry" + policyconfig "github.com/opentdf/platform/service/policy/config" + policydb "github.com/opentdf/platform/service/policy/db" +) + +type RegisteredResourcesService struct { //nolint:revive // RegisteredResourcesService is a valid name + dbClient policydb.PolicyDBClient + logger *logger.Logger + config *policyconfig.Config +} + +func OnConfigUpdate(s *RegisteredResourcesService) serviceregistry.OnConfigUpdateHook { + return func(_ context.Context, cfg config.ServiceConfig) error { + sharedCfg, err := policyconfig.GetSharedPolicyConfig(cfg) + if err != nil { + return fmt.Errorf("failed to get shared policy config: %w", err) + } + s.config = sharedCfg + s.dbClient = policydb.NewClient(s.dbClient.Client, s.logger, int32(sharedCfg.ListRequestLimitMax), int32(sharedCfg.ListRequestLimitDefault)) + + s.logger.Info("registered resources service config reloaded") + + return nil + } +} + +func NewRegistration(ns string, dbRegister serviceregistry.DBRegister) *serviceregistry.Service[registeredresourcesconnect.RegisteredResourcesServiceHandler] { + rrService := new(RegisteredResourcesService) + onUpdateConfigHook := OnConfigUpdate(rrService) + + return &serviceregistry.Service[registeredresourcesconnect.RegisteredResourcesServiceHandler]{ + ServiceOptions: serviceregistry.ServiceOptions[registeredresourcesconnect.RegisteredResourcesServiceHandler]{ + Namespace: ns, + DB: dbRegister, + ServiceDesc: ®isteredresources.RegisteredResourcesService_ServiceDesc, + ConnectRPCFunc: registeredresourcesconnect.NewRegisteredResourcesServiceHandler, + OnConfigUpdate: onUpdateConfigHook, + RegisterFunc: func(srp serviceregistry.RegistrationParams) (registeredresourcesconnect.RegisteredResourcesServiceHandler, serviceregistry.HandlerServer) { + logger := srp.Logger + cfg, err := policyconfig.GetSharedPolicyConfig(srp.Config) + if err != nil { + logger.Error("error getting registered resources service policy config", slog.String("error", err.Error())) + panic(err) + } + + rrService.logger = logger + rrService.dbClient = policydb.NewClient(srp.DBClient, logger, int32(cfg.ListRequestLimitMax), int32(cfg.ListRequestLimitDefault)) + rrService.config = cfg + return rrService, nil + }, + }, + } +} + +func (s *RegisteredResourcesService) IsReady(ctx context.Context) error { + s.logger.TraceContext(ctx, "checking readiness of registered resources service") + if err := s.dbClient.SQLDB.PingContext(ctx); err != nil { + return err + } + + return nil +} + +/// Registered Resources Handlers + +func (s *RegisteredResourcesService) CreateRegisteredResource(ctx context.Context, req *connect.Request[registeredresources.CreateRegisteredResourceRequest]) (*connect.Response[registeredresources.CreateRegisteredResourceResponse], error) { + rsp := ®isteredresources.CreateRegisteredResourceResponse{} + + auditParams := audit.PolicyEventParams{ + ActionType: audit.ActionTypeCreate, + ObjectType: audit.ObjectTypeRegisteredResource, + } + + s.logger.DebugContext(ctx, "creating registered resource", slog.String("name", req.Msg.GetName())) + + err := s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { + resource, err := txClient.CreateRegisteredResource(ctx, req.Msg) + if err != nil { + return err + } + + auditParams.ObjectID = resource.GetId() + auditParams.Original = resource + s.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) + + rsp.Resource = resource + return nil + }) + if err != nil { + s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) + return nil, db.StatusifyError(err, db.ErrTextCreationFailed, slog.String("registered resource", req.Msg.String())) + } + + return connect.NewResponse(rsp), nil +} + +func (s *RegisteredResourcesService) GetRegisteredResource(ctx context.Context, req *connect.Request[registeredresources.GetRegisteredResourceRequest]) (*connect.Response[registeredresources.GetRegisteredResourceResponse], error) { + rsp := ®isteredresources.GetRegisteredResourceResponse{} + + s.logger.DebugContext(ctx, "getting registered resource", slog.Any("identifier", req.Msg.GetIdentifier())) + + resource, err := s.dbClient.GetRegisteredResource(ctx, req.Msg) + if err != nil { + return nil, db.StatusifyError(err, db.ErrTextGetRetrievalFailed, slog.Any("identifier", req.Msg.GetIdentifier())) + } + rsp.Resource = resource + + return connect.NewResponse(rsp), nil +} + +func (s *RegisteredResourcesService) ListRegisteredResources(ctx context.Context, req *connect.Request[registeredresources.ListRegisteredResourcesRequest]) (*connect.Response[registeredresources.ListRegisteredResourcesResponse], error) { + s.logger.DebugContext(ctx, "listing registered resources") + + rsp, err := s.dbClient.ListRegisteredResources(ctx, req.Msg) + if err != nil { + return nil, db.StatusifyError(err, db.ErrTextListRetrievalFailed) + } + + s.logger.DebugContext(ctx, "listed registered resources") + + return connect.NewResponse(rsp), nil +} + +func (s *RegisteredResourcesService) UpdateRegisteredResource(ctx context.Context, req *connect.Request[registeredresources.UpdateRegisteredResourceRequest]) (*connect.Response[registeredresources.UpdateRegisteredResourceResponse], error) { + resourceID := req.Msg.GetId() + + rsp := ®isteredresources.UpdateRegisteredResourceResponse{} + + auditParams := audit.PolicyEventParams{ + ActionType: audit.ActionTypeUpdate, + ObjectType: audit.ObjectTypeRegisteredResource, + ObjectID: resourceID, + } + + s.logger.DebugContext(ctx, "updating registered resource", slog.String("id", resourceID)) + + err := s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { + original, err := txClient.GetRegisteredResource(ctx, ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_ResourceId{ + ResourceId: resourceID, + }, + }) + if err != nil { + return err + } + + updated, err := txClient.UpdateRegisteredResource(ctx, req.Msg) + if err != nil { + return err + } + + auditParams.Original = original + auditParams.Updated = updated + s.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) + + rsp.Resource = updated + return nil + }) + if err != nil { + s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) + return nil, db.StatusifyError(err, db.ErrTextUpdateFailed, slog.String("registered resource", req.Msg.String())) + } + + return connect.NewResponse(rsp), nil +} + +func (s *RegisteredResourcesService) DeleteRegisteredResource(ctx context.Context, req *connect.Request[registeredresources.DeleteRegisteredResourceRequest]) (*connect.Response[registeredresources.DeleteRegisteredResourceResponse], error) { + resourceID := req.Msg.GetId() + + rsp := ®isteredresources.DeleteRegisteredResourceResponse{} + + auditParams := audit.PolicyEventParams{ + ActionType: audit.ActionTypeDelete, + ObjectType: audit.ObjectTypeRegisteredResource, + ObjectID: resourceID, + } + + s.logger.DebugContext(ctx, "deleting registered resource", slog.String("id", resourceID)) + + deleted, err := s.dbClient.DeleteRegisteredResource(ctx, resourceID) + if err != nil { + s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) + return nil, db.StatusifyError(err, db.ErrTextDeletionFailed, slog.String("registered resource", req.Msg.String())) + } + + s.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) + + rsp.Resource = deleted + + return connect.NewResponse(rsp), nil +} + +/// Registered Resource Values Handlers + +func (s *RegisteredResourcesService) CreateRegisteredResourceValue(ctx context.Context, req *connect.Request[registeredresources.CreateRegisteredResourceValueRequest]) (*connect.Response[registeredresources.CreateRegisteredResourceValueResponse], error) { + rsp := ®isteredresources.CreateRegisteredResourceValueResponse{} + + auditParams := audit.PolicyEventParams{ + ActionType: audit.ActionTypeCreate, + ObjectType: audit.ObjectTypeRegisteredResourceValue, + } + + s.logger.DebugContext(ctx, "creating registered resource value", slog.String("value", req.Msg.GetValue())) + + err := s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { + value, err := txClient.CreateRegisteredResourceValue(ctx, req.Msg) + if err != nil { + return err + } + + auditParams.ObjectID = value.GetId() + auditParams.Original = value + s.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) + + rsp.Value = value + return nil + }) + if err != nil { + s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) + return nil, db.StatusifyError(err, db.ErrTextCreationFailed, slog.String("registered resource value", req.Msg.String())) + } + + return connect.NewResponse(rsp), nil +} + +func (s *RegisteredResourcesService) GetRegisteredResourceValue(ctx context.Context, req *connect.Request[registeredresources.GetRegisteredResourceValueRequest]) (*connect.Response[registeredresources.GetRegisteredResourceValueResponse], error) { + rsp := ®isteredresources.GetRegisteredResourceValueResponse{} + + s.logger.DebugContext(ctx, "getting registered resource value", slog.Any("identifier", req.Msg.GetIdentifier())) + + value, err := s.dbClient.GetRegisteredResourceValue(ctx, req.Msg) + if err != nil { + return nil, db.StatusifyError(err, db.ErrTextGetRetrievalFailed, slog.Any("identifier", req.Msg.GetIdentifier())) + } + rsp.Value = value + + return connect.NewResponse(rsp), nil +} + +func (s *RegisteredResourcesService) ListRegisteredResourceValues(ctx context.Context, req *connect.Request[registeredresources.ListRegisteredResourceValuesRequest]) (*connect.Response[registeredresources.ListRegisteredResourceValuesResponse], error) { + s.logger.DebugContext(ctx, "listing registered resource values") + + rsp, err := s.dbClient.ListRegisteredResourceValues(ctx, req.Msg) + if err != nil { + return nil, db.StatusifyError(err, db.ErrTextListRetrievalFailed) + } + + s.logger.DebugContext(ctx, "listed registered resource values") + + return connect.NewResponse(rsp), nil +} + +func (s *RegisteredResourcesService) UpdateRegisteredResourceValue(ctx context.Context, req *connect.Request[registeredresources.UpdateRegisteredResourceValueRequest]) (*connect.Response[registeredresources.UpdateRegisteredResourceValueResponse], error) { + valueID := req.Msg.GetId() + + rsp := ®isteredresources.UpdateRegisteredResourceValueResponse{} + + auditParams := audit.PolicyEventParams{ + ActionType: audit.ActionTypeUpdate, + ObjectType: audit.ObjectTypeRegisteredResourceValue, + ObjectID: valueID, + } + + s.logger.DebugContext(ctx, "updating registered resource value", slog.String("id", valueID)) + + err := s.dbClient.RunInTx(ctx, func(txClient *policydb.PolicyDBClient) error { + original, err := txClient.GetRegisteredResourceValue(ctx, ®isteredresources.GetRegisteredResourceValueRequest{ + Identifier: ®isteredresources.GetRegisteredResourceValueRequest_ValueId{ + ValueId: valueID, + }, + }) + if err != nil { + return err + } + + updated, err := txClient.UpdateRegisteredResourceValue(ctx, req.Msg) + if err != nil { + return err + } + + auditParams.Original = original + auditParams.Updated = updated + s.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) + + rsp.Value = updated + + return nil + }) + if err != nil { + s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) + return nil, db.StatusifyError(err, db.ErrTextUpdateFailed, slog.String("registered resource value", req.Msg.String())) + } + + return connect.NewResponse(rsp), nil +} + +func (s *RegisteredResourcesService) DeleteRegisteredResourceValue(ctx context.Context, req *connect.Request[registeredresources.DeleteRegisteredResourceValueRequest]) (*connect.Response[registeredresources.DeleteRegisteredResourceValueResponse], error) { + valueID := req.Msg.GetId() + + rsp := ®isteredresources.DeleteRegisteredResourceValueResponse{} + + auditParams := audit.PolicyEventParams{ + ActionType: audit.ActionTypeDelete, + ObjectType: audit.ObjectTypeRegisteredResourceValue, + ObjectID: valueID, + } + + s.logger.DebugContext(ctx, "deleting registered resource value", slog.String("id", valueID)) + + deleted, err := s.dbClient.DeleteRegisteredResourceValue(ctx, valueID) + if err != nil { + s.logger.Audit.PolicyCRUDFailure(ctx, auditParams) + return nil, db.StatusifyError(err, db.ErrTextDeletionFailed, slog.String("registered resource value", req.Msg.String())) + } + + s.logger.Audit.PolicyCRUDSuccess(ctx, auditParams) + + rsp.Value = deleted + + return connect.NewResponse(rsp), nil +} diff --git a/service/policy/registeredresources/registered_resources_test.go b/service/policy/registeredresources/registered_resources_test.go new file mode 100644 index 0000000000..f7ae25a0fb --- /dev/null +++ b/service/policy/registeredresources/registered_resources_test.go @@ -0,0 +1,703 @@ +package registeredresources + +import ( + "strings" + "testing" + + "github.com/bufbuild/protovalidate-go" + "github.com/opentdf/platform/protocol/go/policy/registeredresources" + "github.com/stretchr/testify/suite" +) + +type RegisteredResourcesSuite struct { + suite.Suite + v protovalidate.Validator +} + +func (s *RegisteredResourcesSuite) SetupSuite() { + v, err := protovalidate.New() + if err != nil { + panic(err) + } + s.v = v +} + +func TestRegisteredResourcesServiceProtos(t *testing.T) { + suite.Run(t, new(RegisteredResourcesSuite)) +} + +const ( + validName = "name" + validValue = "value" + validUUID = "00000000-0000-0000-0000-000000000000" + validURI = "https://ndr-uri" + + invalidUUID = "not-uuid" + invalidURI = "not-uri" + + errMsgRequired = "required" + errMsgOneOfRequired = "oneof [required]" + errMsgUUID = "string.uuid" + errMsgOptionalUUID = "optional_uuid_format" + errMsgURI = "string.uri" + errMsgNameFormat = "rr_name_format" + errMsgValueFormat = "rr_value_format" + errMsgStringPattern = "string.pattern" + errMsgStringMaxLen = "string.max_len" +) + +/// +/// Registered Resources +/// + +// Create + +func (s *RegisteredResourcesSuite) TestCreateRegisteredResource_Valid_Succeeds() { + testCases := []struct { + name string + req *registeredresources.CreateRegisteredResourceRequest + }{ + { + name: "Name Only", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: validName, + }, + }, + { + name: "Name with Values", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: validName, + Values: []string{ + validValue, + }, + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().NoError(err) + }) + } +} + +func (s *RegisteredResourcesSuite) TestCreateRegisteredResource_Invalid_Fails() { + testCases := []struct { + name string + req *registeredresources.CreateRegisteredResourceRequest + errMsg string + }{ + { + name: "Missing Name", + req: ®isteredresources.CreateRegisteredResourceRequest{}, + errMsg: errMsgRequired, + }, + { + name: "Invalid Name (space)", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: " ", + }, + errMsg: errMsgNameFormat, + }, + { + name: "Invalid Name (too long)", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: strings.Repeat("a", 254), + }, + errMsg: errMsgStringMaxLen, + }, + { + name: "Invalid Name (text with spaces)", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: "invalid name", + }, + errMsg: errMsgNameFormat, + }, + { + name: "Invalid Name (text with special chars)", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: "invalid@name", + }, + errMsg: errMsgNameFormat, + }, + { + name: "Invalid Name (leading underscore)", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: "_invalid_name", + }, + errMsg: errMsgNameFormat, + }, + { + name: "Invalid Name (trailing underscore)", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: "invalid_name_", + }, + errMsg: errMsgNameFormat, + }, + { + name: "Invalid Name (leading hyphen)", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: "-invalid-name", + }, + errMsg: errMsgNameFormat, + }, + { + name: "Invalid Name (trailing hyphen)", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: "invalid-name-", + }, + errMsg: errMsgNameFormat, + }, + { + name: "Invalid Name (invalid values)", + req: ®isteredresources.CreateRegisteredResourceRequest{ + Name: validName, + Values: []string{ + "invalid value", + }, + }, + errMsg: errMsgStringPattern, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.errMsg) + }) + } +} + +// Get + +func (s *RegisteredResourcesSuite) TestGetRegisteredResource_Valid_Succeeds() { + testCases := []struct { + name string + req *registeredresources.GetRegisteredResourceRequest + }{ + { + name: "Identifier (UUID)", + req: ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_ResourceId{ + ResourceId: validUUID, + }, + }, + }, + { + name: "Identifier (FQN)", + req: ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Fqn{ + Fqn: validURI, + }, + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().NoError(err) + }) + } +} + +func (s *RegisteredResourcesSuite) TestGetRegisteredResource_Invalid_Fails() { + testCases := []struct { + name string + req *registeredresources.GetRegisteredResourceRequest + errMsg string + }{ + { + name: "Missing Identifier", + req: ®isteredresources.GetRegisteredResourceRequest{}, + errMsg: errMsgOneOfRequired, + }, + { + name: "Invalid UUID", + req: ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_ResourceId{ + ResourceId: invalidUUID, + }, + }, + errMsg: errMsgUUID, + }, + { + name: "Invalid FQN", + req: ®isteredresources.GetRegisteredResourceRequest{ + Identifier: ®isteredresources.GetRegisteredResourceRequest_Fqn{ + Fqn: invalidURI, + }, + }, + errMsg: errMsgURI, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.errMsg) + }) + } +} + +// Update + +func (s *RegisteredResourcesSuite) TestUpdateRegisteredResource_Valid_Succeeds() { + // id provided + // valid value provided + testCases := []struct { + name string + req *registeredresources.UpdateRegisteredResourceRequest + }{ + { + name: "ID only", + req: ®isteredresources.UpdateRegisteredResourceRequest{ + Id: validUUID, + }, + }, + { + name: "ID with Name", + req: ®isteredresources.UpdateRegisteredResourceRequest{ + Id: validUUID, + Name: validName, + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().NoError(err) + }) + } +} + +func (s *RegisteredResourcesSuite) TestUpdateRegisteredResource_Invalid_Fails() { + testCases := []struct { + name string + req *registeredresources.UpdateRegisteredResourceRequest + errMsg string + }{ + { + name: "Missing ID", + req: ®isteredresources.UpdateRegisteredResourceRequest{}, + errMsg: errMsgUUID, + }, + { + name: "Invalid ID", + req: ®isteredresources.UpdateRegisteredResourceRequest{ + Id: invalidUUID, + }, + errMsg: errMsgUUID, + }, + { + name: "Invalid Name (space)", + req: ®isteredresources.UpdateRegisteredResourceRequest{ + Id: validUUID, + Name: " ", + }, + errMsg: errMsgNameFormat, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.errMsg) + }) + } +} + +// Delete + +func (s *RegisteredResourcesSuite) TestDeleteRegisteredResource_Valid_Succeeds() { + req := ®isteredresources.DeleteRegisteredResourceRequest{ + Id: validUUID, + } + + err := s.v.Validate(req) + + s.Require().NoError(err) +} + +func (s *RegisteredResourcesSuite) TestDeleteRegisteredResource_Invalid_Fails() { + testCases := []struct { + name string + req *registeredresources.DeleteRegisteredResourceRequest + }{ + { + name: "Missing UUID", + req: ®isteredresources.DeleteRegisteredResourceRequest{}, + }, + { + name: "Invalid UUID", + req: ®isteredresources.DeleteRegisteredResourceRequest{ + Id: invalidUUID, + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().Error(err) + s.Require().Contains(err.Error(), errMsgUUID) + }) + } +} + +/// +/// Registered Resource Values +/// + +// Create + +func (s *RegisteredResourcesSuite) TestCreateRegisteredResourceValue_Valid_Succeeds() { + req := ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: validUUID, + Value: validValue, + } + + err := s.v.Validate(req) + + s.Require().NoError(err) +} + +func (s *RegisteredResourcesSuite) TestCreateRegisteredResourceValue_Invalid_Succeeds() { + testCases := []struct { + name string + req *registeredresources.CreateRegisteredResourceValueRequest + errMsg string + }{ + { + name: "Missing Group ID", + req: ®isteredresources.CreateRegisteredResourceValueRequest{ + Value: validValue, + }, + errMsg: errMsgUUID, + }, + { + name: "Invalid Group ID", + req: ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: invalidUUID, + Value: validValue, + }, + errMsg: errMsgUUID, + }, + { + name: "Missing Value", + req: ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: validUUID, + }, + errMsg: errMsgRequired, + }, + { + name: "Invalid Value (space)", + req: ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: validUUID, + Value: " ", + }, + errMsg: errMsgValueFormat, + }, + { + name: "Invalid Value (too long)", + req: ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: validUUID, + Value: strings.Repeat("a", 254), + }, + errMsg: errMsgStringMaxLen, + }, + { + name: "Invalid Value (text with spaces)", + req: ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: validUUID, + Value: "invalid value", + }, + errMsg: errMsgValueFormat, + }, + { + name: "Invalid Value (text with special chars)", + req: ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: validUUID, + Value: "invalid@value", + }, + errMsg: errMsgValueFormat, + }, + { + name: "Invalid Value (leading underscore)", + req: ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: validUUID, + Value: "_invalid_value", + }, + errMsg: errMsgValueFormat, + }, + { + name: "Invalid Value (trailing underscore)", + req: ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: validUUID, + Value: "invalid_value_", + }, + errMsg: errMsgValueFormat, + }, + { + name: "Invalid Value (leading hyphen)", + req: ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: validUUID, + Value: "-invalid-value", + }, + errMsg: errMsgValueFormat, + }, + { + name: "Invalid Value (trailing hyphen)", + req: ®isteredresources.CreateRegisteredResourceValueRequest{ + ResourceId: validUUID, + Value: "invalid-value-", + }, + errMsg: errMsgValueFormat, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.errMsg) + }) + } +} + +// Get + +func (s *RegisteredResourcesSuite) TestGetRegisteredResourceValue_Valid_Succeeds() { + testCases := []struct { + name string + req *registeredresources.GetRegisteredResourceValueRequest + }{ + { + name: "Identifier (UUID)", + req: ®isteredresources.GetRegisteredResourceValueRequest{ + Identifier: ®isteredresources.GetRegisteredResourceValueRequest_ValueId{ + ValueId: validUUID, + }, + }, + }, + { + name: "Identifier (FQN)", + req: ®isteredresources.GetRegisteredResourceValueRequest{ + Identifier: ®isteredresources.GetRegisteredResourceValueRequest_Fqn{ + Fqn: validURI, + }, + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().NoError(err) + }) + } +} + +func (s *RegisteredResourcesSuite) TestGetRegisteredResourceValue_Invalid_Fails() { + testCases := []struct { + name string + req *registeredresources.GetRegisteredResourceValueRequest + errMsg string + }{ + { + name: "Missing Identifier", + req: ®isteredresources.GetRegisteredResourceValueRequest{}, + errMsg: errMsgOneOfRequired, + }, + { + name: "Invalid UUID", + req: ®isteredresources.GetRegisteredResourceValueRequest{ + Identifier: ®isteredresources.GetRegisteredResourceValueRequest_ValueId{ + ValueId: invalidUUID, + }, + }, + errMsg: errMsgUUID, + }, + { + name: "Invalid FQN", + req: ®isteredresources.GetRegisteredResourceValueRequest{ + Identifier: ®isteredresources.GetRegisteredResourceValueRequest_Fqn{ + Fqn: invalidURI, + }, + }, + errMsg: errMsgURI, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.errMsg) + }) + } +} + +// List + +func (s *RegisteredResourcesSuite) TestListRegisteredResourceValues_Valid_Succeeds() { + testCases := []struct { + name string + req *registeredresources.ListRegisteredResourceValuesRequest + }{ + { + name: "Missing Group ID", + req: ®isteredresources.ListRegisteredResourceValuesRequest{}, + }, + { + name: "Group ID", + req: ®isteredresources.ListRegisteredResourceValuesRequest{ + ResourceId: validUUID, + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().NoError(err) + }) + } +} + +func (s *RegisteredResourcesSuite) TestListRegisteredResourceValues_Invalid_Succeeds() { + req := ®isteredresources.ListRegisteredResourceValuesRequest{ + ResourceId: invalidUUID, + } + + err := s.v.Validate(req) + + s.Require().Error(err) + s.Require().ErrorContains(err, errMsgOptionalUUID) +} + +// Update + +func (s *RegisteredResourcesSuite) TestUpdateRegisteredResourceValue_Valid_Succeeds() { + // id provided + // valid value provided + testCases := []struct { + name string + req *registeredresources.UpdateRegisteredResourceValueRequest + }{ + { + name: "ID only", + req: ®isteredresources.UpdateRegisteredResourceValueRequest{ + Id: validUUID, + }, + }, + { + name: "ID with Value", + req: ®isteredresources.UpdateRegisteredResourceValueRequest{ + Id: validUUID, + Value: validValue, + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().NoError(err) + }) + } +} + +func (s *RegisteredResourcesSuite) TestUpdateRegisteredResourceValue_Invalid_Fails() { + testCases := []struct { + name string + req *registeredresources.UpdateRegisteredResourceValueRequest + errMsg string + }{ + { + name: "Missing ID", + req: ®isteredresources.UpdateRegisteredResourceValueRequest{}, + errMsg: errMsgUUID, + }, + { + name: "Invalid ID", + req: ®isteredresources.UpdateRegisteredResourceValueRequest{ + Id: invalidUUID, + }, + errMsg: errMsgUUID, + }, + { + name: "Invalid Value (space)", + req: ®isteredresources.UpdateRegisteredResourceValueRequest{ + Id: validUUID, + Value: " ", + }, + errMsg: errMsgValueFormat, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().Error(err) + s.Require().Contains(err.Error(), tc.errMsg) + }) + } +} + +// Delete + +func (s *RegisteredResourcesSuite) TestDeleteRegisteredResourceValue_Valid_Succeeds() { + req := ®isteredresources.DeleteRegisteredResourceValueRequest{ + Id: validUUID, + } + + err := s.v.Validate(req) + + s.Require().NoError(err) +} + +func (s *RegisteredResourcesSuite) TestDeleteRegisteredResourceValue_Invalid_Fails() { + testCases := []struct { + name string + req *registeredresources.DeleteRegisteredResourceValueRequest + }{ + { + name: "Missing UUID", + req: ®isteredresources.DeleteRegisteredResourceValueRequest{}, + }, + { + name: "Invalid UUID", + req: ®isteredresources.DeleteRegisteredResourceValueRequest{ + Id: invalidUUID, + }, + }, + } + + for _, tc := range testCases { + s.Run(tc.name, func() { + err := s.v.Validate(tc.req) + + s.Require().Error(err) + s.Require().Contains(err.Error(), errMsgUUID) + }) + } +}