package api import ( "context" "errors" "fmt" "github.com/google/uuid" "testing" "time" "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/audit/messaging" auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-core-platform/common-audit.git/gen/go/audit/v1" "github.com/Azure/go-amqp" "github.com/bufbuild/protovalidate-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "google.golang.org/protobuf/proto" ) func TestRoutableAuditApi(t *testing.T) { // Specify test timeout ctx, cancelFn := context.WithTimeout(context.Background(), 120*time.Second) defer cancelFn() // Start solace docker container solaceContainer, err := messaging.NewSolaceContainer(context.Background()) assert.NoError(t, err) defer solaceContainer.Stop() // Instantiate the messaging api messagingApi, err := messaging.NewAmqpMessagingApi(messaging.AmqpConfig{URL: solaceContainer.AmqpConnectionString}) assert.NoError(t, err) // Validator validator, err := protovalidate.New() assert.NoError(t, err) // Instantiate the audit api organizationTopicPrefix := "org" projectTopicPrefix := "project" systemTopicName := "topic://system/admin-events" auditApi, err := newRoutableAuditApi( messagingApi, topicNameConfig{ OrganizationTopicPrefix: organizationTopicPrefix, ProjectTopicPrefix: projectTopicPrefix, SystemTopicName: systemTopicName}, validator, ) assert.NoError(t, err) // Check logging of organization events t.Run("Log public organization event", func(t *testing.T) { defer solaceContainer.StopOnError() // Create the queue and topic subscription in solace queueName := "org-event-public" assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*")) // Instantiate test data event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PUBLIC assert.NoError(t, (*auditApi).Log( ctx, event, visibility, routingIdentifier, objectIdentifier, )) message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "ORGANIZATION_CREATED", visibility) }) t.Run("Log private organization event", func(t *testing.T) { defer solaceContainer.StopOnError() queueName := "org-event-private" assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*")) // Instantiate test data event, routingIdentifier, objectIdentifier := NewOrganizationAuditEvent(nil) topicName := fmt.Sprintf("org/%s", routingIdentifier.Identifier) assert.NoError( t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicName)) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PRIVATE assert.NoError(t, (*auditApi).Log( ctx, event, visibility, routingIdentifier, objectIdentifier, )) // Receive the event from solace message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "ORGANIZATION_CREATED", visibility) }) // Check logging of folder events t.Run("Log public folder event", func(t *testing.T) { defer solaceContainer.StopOnError() // Create the queue and topic subscription in solace queueName := "folder-event-public" assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*")) // Instantiate test data event, routingIdentifier, objectIdentifier := NewFolderAuditEvent(nil) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PUBLIC assert.NoError(t, (*auditApi).Log( ctx, event, visibility, routingIdentifier, objectIdentifier, )) message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "FOLDER_CREATED", visibility) }) t.Run("Log private folder event", func(t *testing.T) { defer solaceContainer.StopOnError() queueName := "folder-event-private" assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*")) // Instantiate test data event, routingIdentifier, objectIdentifier := NewFolderAuditEvent(nil) topicName := fmt.Sprintf("org/%s", routingIdentifier.Identifier) assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicName)) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PRIVATE assert.NoError(t, (*auditApi).Log( ctx, event, visibility, routingIdentifier, objectIdentifier, )) // Receive the event from solace message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "FOLDER_CREATED", visibility) }) // Check logging of project events t.Run("Log public project event", func(t *testing.T) { defer solaceContainer.StopOnError() queueName := "project-event-public" assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "project/*")) // Instantiate test data event, routingIdentifier, objectIdentifier := NewProjectAuditEvent(nil) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PUBLIC assert.NoError(t, (*auditApi).Log( ctx, event, visibility, routingIdentifier, objectIdentifier, )) // Receive the event from solace message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, projectTopicPrefix, message, routingIdentifier, objectIdentifier, event, "PROJECT_CREATED", visibility) }) t.Run("Log private project event", func(t *testing.T) { defer solaceContainer.StopOnError() queueName := "project-event-private" assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "project/*")) // Instantiate test data event, routingIdentifier, objectIdentifier := NewProjectAuditEvent(nil) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PRIVATE assert.NoError(t, (*auditApi).Log( ctx, event, visibility, routingIdentifier, objectIdentifier, )) // Receive the event from solace message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, projectTopicPrefix, message, routingIdentifier, objectIdentifier, event, "PROJECT_CREATED", visibility) }) // Check logging of system events t.Run("Log private system event", func(t *testing.T) { defer solaceContainer.StopOnError() queueName := "system-event-private" assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "system/*")) // Instantiate test data event := NewSystemAuditEvent(nil) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PRIVATE assert.NoError(t, (*auditApi).Log( ctx, event, visibility, nil, nil, )) // Receive the event from solace message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) // Check topic name assert.Equal(t, systemTopicName, *message.Properties.To) // Check cloud event properties applicationProperties := message.ApplicationProperties assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"]) assert.Equal(t, "resource-manager", applicationProperties["cloudEvents:source"]) _, isUuid := uuid.Parse(fmt.Sprintf("%s", applicationProperties["cloudEvents:id"])) assert.True(t, true, isUuid) assert.Equal(t, event.EventTimeStamp.AsTime().UnixMilli(), applicationProperties["cloudEvents:time"]) assert.Equal(t, "application/cloudevents+protobuf", applicationProperties["cloudEvents:datacontenttype"]) assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"]) // Check deserialized message validateRoutableEventPayload( t, message.Data[0], nil, nil, event, "SYSTEM_CHANGED", visibility) }) // Check logging of organization events t.Run("Log event with details", func(t *testing.T) { defer solaceContainer.StopOnError() // Create the queue and topic subscription in solace queueName := "org-event-with-details" assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*")) // Instantiate test data event, routingIdentifier, objectIdentifier := NewOrganizationAuditEventWithDetails() // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PUBLIC assert.NoError(t, (*auditApi).Log( ctx, event, visibility, routingIdentifier, objectIdentifier, )) message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, organizationTopicPrefix, message, routingIdentifier, objectIdentifier, event, "ORGANIZATION_CREATED", visibility) }) } func validateSentEvent( t *testing.T, topicPrefix string, message *amqp.Message, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier, event *auditV1.AuditEvent, eventName string, visibility auditV1.Visibility, ) { // Check topic name assert.Equal(t, fmt.Sprintf("topic://%s/%s", topicPrefix, routingIdentifier.Identifier), *message.Properties.To) // Check cloud event properties applicationProperties := message.ApplicationProperties assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"]) assert.Equal(t, "resource-manager", applicationProperties["cloudEvents:source"]) _, isUuid := uuid.Parse(fmt.Sprintf("%s", applicationProperties["cloudEvents:id"])) assert.True(t, true, isUuid) assert.Equal(t, event.EventTimeStamp.AsTime().UnixMilli(), applicationProperties["cloudEvents:time"]) assert.Equal(t, "application/cloudevents+protobuf", applicationProperties["cloudEvents:datacontenttype"]) assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"]) // Check deserialized message validateRoutableEventPayload( t, message.Data[0], routingIdentifier, objectIdentifier, event, eventName, visibility) } func validateRoutableEventPayload( t *testing.T, payload []byte, routingIdentifier *RoutingIdentifier, objectIdentifier *auditV1.ObjectIdentifier, event *auditV1.AuditEvent, eventName string, visibility auditV1.Visibility, ) { // Check routable audit event parameters var routableAuditEvent auditV1.RoutableAuditEvent assert.NoError(t, proto.Unmarshal(payload, &routableAuditEvent)) assert.Equal(t, eventName, routableAuditEvent.EventName) assert.Equal(t, visibility, routableAuditEvent.Visibility) switch reference := routableAuditEvent.ResourceReference.(type) { case *auditV1.RoutableAuditEvent_ObjectIdentifier: assert.True(t, proto.Equal(objectIdentifier, reference.ObjectIdentifier)) switch routingIdentifier.Type { case RoutingIdentifierTypeOrganization: if objectIdentifier.Type == auditV1.ObjectType_OBJECT_TYPE_ORGANIZATION { assert.Equal(t, routingIdentifier.Identifier.String(), reference.ObjectIdentifier.Identifier) } case RoutingIdentifierTypeProject: assert.Equal(t, routingIdentifier.Identifier.String(), reference.ObjectIdentifier.Identifier) assert.Equal(t, auditV1.ObjectType_OBJECT_TYPE_PROJECT, reference.ObjectIdentifier.Type) default: assert.Fail(t, "Routing identifier type not expected") } case *auditV1.RoutableAuditEvent_ObjectName: assert.Nil(t, objectIdentifier) assert.Nil(t, routingIdentifier) assert.Equal(t, auditV1.ObjectName_OBJECT_NAME_SYSTEM, reference.ObjectName) default: assert.Fail(t, "Object name not expected") } var auditEvent auditV1.AuditEvent switch data := routableAuditEvent.Data.(type) { case *auditV1.RoutableAuditEvent_UnencryptedData: assert.NoError(t, proto.Unmarshal(data.UnencryptedData.Data, &auditEvent)) default: assert.Fail(t, "Encrypted data not expected") } // Check audit event assert.True(t, proto.Equal(event, &auditEvent)) } func TestRoutableTopicNameResolver_Resolve_UnsupportedIdentifierType(t *testing.T) { resolver := routableTopicNameResolver{} _, err := resolver.Resolve(&RoutingIdentifier{Type: 2}) assert.ErrorIs(t, err, ErrUnsupportedObjectIdentifierType) } func TestNewRoutableAuditApi_NewRoutableAuditApi_MessagingApiNil(t *testing.T) { auditApi, err := newRoutableAuditApi(nil, topicNameConfig{}, nil) assert.Nil(t, auditApi) assert.EqualError(t, err, "messaging api nil") } func TestRoutableAuditApi_ValidateAndSerialize_ValidationFailed(t *testing.T) { expectedError := errors.New("expected error") validator := &ProtobufValidatorMock{} validator.On("Validate", mock.Anything).Return(expectedError) var protobufValidator ProtobufValidator = validator auditApi := routableAuditApi{validator: &protobufValidator} event := NewSystemAuditEvent(nil) _, err := auditApi.ValidateAndSerialize(event, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil) assert.ErrorIs(t, err, expectedError) } func TestRoutableAuditApi_Log_ValidationFailed(t *testing.T) { expectedError := errors.New("expected error") validator := &ProtobufValidatorMock{} validator.On("Validate", mock.Anything).Return(expectedError) var protobufValidator ProtobufValidator = validator auditApi := routableAuditApi{validator: &protobufValidator} event := NewSystemAuditEvent(nil) err := auditApi.Log(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil) assert.ErrorIs(t, err, expectedError) } func TestRoutableAuditApi_Log_NilEvent(t *testing.T) { auditApi := routableAuditApi{} err := auditApi.Log(context.Background(), nil, auditV1.Visibility_VISIBILITY_PUBLIC, nil, nil) assert.ErrorIs(t, err, ErrEventNil) }