package api import ( "context" "errors" "fmt" "strings" "testing" "time" "github.com/google/uuid" "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/audit/messaging" auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.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.NewAmqpApi(messaging.AmqpConfig{URL: solaceContainer.AmqpConnectionString}) assert.NoError(t, err) // Validator validator, err := protovalidate.New() assert.NoError(t, err) traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE" // Instantiate the audit api organizationTopicPrefix := "org" projectTopicPrefix := "project" folderTopicPrefix := "folder" systemTopicName := "topic://system/admin-events" auditApi, err := newRoutableAuditApi( messagingApi, topicNameConfig{ FolderTopicPrefix: folderTopicPrefix, OrganizationTopicPrefix: organizationTopicPrefix, ProjectTopicPrefix: projectTopicPrefix, SystemTopicName: systemTopicName}, validator, ) assert.NoError(t, err) // Check that event-type data-access is rejected as it is currently // not supported by downstream services t.Run("reject data access event", func(t *testing.T) { defer solaceContainer.StopOnError() // Create the queue and topic subscription in solace queueName := "org-reject-data-access" assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "org/*")) // Instantiate test data event, objectIdentifier := newOrganizationAuditEvent(nil) event.LogName = strings.Replace(event.LogName, string(EventTypeAdminActivity), string(EventTypeDataAccess), 1) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PUBLIC assert.ErrorIs(t, (*auditApi).LogWithTrace( ctx, event, visibility, NewRoutableIdentifier(objectIdentifier), &traceParent, &traceState, ), ErrUnsupportedEventTypeDataAccess) }) // 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, objectIdentifier := newOrganizationAuditEvent(nil) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PUBLIC assert.NoError(t, (*auditApi).LogWithTrace( ctx, event, visibility, NewRoutableIdentifier(objectIdentifier), &traceParent, &traceState, )) message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, organizationTopicPrefix, message, objectIdentifier, event, "stackit.resourcemanager.v2.organization.created", visibility, &traceParent, &traceState) }) 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, objectIdentifier := newOrganizationAuditEvent(nil) topicName := fmt.Sprintf("org/%s", objectIdentifier.Identifier) assert.NoError( t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicName)) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PRIVATE assert.NoError(t, (*auditApi).LogWithTrace( ctx, event, visibility, NewRoutableIdentifier(objectIdentifier), &traceParent, &traceState, )) // Receive the event from solace message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, organizationTopicPrefix, message, objectIdentifier, event, "stackit.resourcemanager.v2.organization.created", visibility, &traceParent, &traceState) }) // 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, "folder/*")) // Instantiate test data event, objectIdentifier := newFolderAuditEvent(nil) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PUBLIC assert.NoError(t, (*auditApi).LogWithTrace( ctx, event, visibility, NewRoutableIdentifier(objectIdentifier), &traceParent, &traceState, )) message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, folderTopicPrefix, message, objectIdentifier, event, "stackit.resourcemanager.v2.folder.created", visibility, &traceParent, &traceState) }) 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, "folder/*")) // Instantiate test data event, objectIdentifier := newFolderAuditEvent(nil) topicName := fmt.Sprintf("folder/%s", objectIdentifier.Identifier) assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, topicName)) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PRIVATE assert.NoError(t, (*auditApi).LogWithTrace( ctx, event, visibility, NewRoutableIdentifier(objectIdentifier), &traceParent, &traceState, )) // Receive the event from solace message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, folderTopicPrefix, message, objectIdentifier, event, "stackit.resourcemanager.v2.folder.created", visibility, &traceParent, &traceState) }) // 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, objectIdentifier := newProjectAuditEvent(nil) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PUBLIC assert.NoError(t, (*auditApi).LogWithTrace( ctx, event, visibility, NewRoutableIdentifier(objectIdentifier), &traceParent, &traceState, )) // Receive the event from solace message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, projectTopicPrefix, message, objectIdentifier, event, "stackit.resourcemanager.v2.project.created", visibility, &traceParent, &traceState) }) 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, objectIdentifier := newProjectAuditEvent(nil) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PRIVATE assert.NoError(t, (*auditApi).LogWithTrace( ctx, event, visibility, NewRoutableIdentifier(objectIdentifier), &traceParent, &traceState, )) // Receive the event from solace message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, projectTopicPrefix, message, objectIdentifier, event, "stackit.resourcemanager.v2.project.created", visibility, &traceParent, &traceState) }) // Check logging of system events with identifier t.Run("Log private project system event", func(t *testing.T) { defer solaceContainer.StopOnError() queueName := "project-system-event-private" assert.NoError(t, solaceContainer.QueueCreate(ctx, queueName)) assert.NoError(t, solaceContainer.TopicSubscriptionCreate(ctx, queueName, "system/*")) // Instantiate test data event := newProjectSystemAuditEvent(nil) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PRIVATE assert.NoError(t, (*auditApi).LogWithTrace( ctx, event, visibility, RoutableSystemIdentifier, 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.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime().UnixMilli(), applicationProperties["cloudEvents:time"]) assert.Equal(t, "application/cloudevents+protobuf", applicationProperties["cloudEvents:datacontenttype"]) assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"]) assert.Nil(t, applicationProperties["cloudEvents:traceparent"]) assert.Nil(t, applicationProperties["cloudEvents:tracestate"]) // Check deserialized message validateRoutableEventPayload( t, message.Data[0], RoutableSystemIdentifier.ToObjectIdentifier(), event, "stackit.resourcemanager.v2.system.changed", 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).LogWithTrace( ctx, event, visibility, RoutableSystemIdentifier, 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.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime().UnixMilli(), applicationProperties["cloudEvents:time"]) assert.Equal(t, "application/cloudevents+protobuf", applicationProperties["cloudEvents:datacontenttype"]) assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"]) assert.Nil(t, applicationProperties["cloudEvents:traceparent"]) assert.Nil(t, applicationProperties["cloudEvents:tracestate"]) // Check deserialized message validateRoutableEventPayload( t, message.Data[0], SystemIdentifier, event, "stackit.resourcemanager.v2.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, objectIdentifier := newOrganizationAuditEvent(nil) // Log the event to solace visibility := auditV1.Visibility_VISIBILITY_PUBLIC assert.NoError(t, (*auditApi).LogWithTrace( ctx, event, visibility, NewRoutableIdentifier(objectIdentifier), &traceParent, &traceState, )) message, err := solaceContainer.NextMessageFromQueue(ctx, queueName, true) assert.NoError(t, err) validateSentEvent( t, organizationTopicPrefix, message, objectIdentifier, event, "stackit.resourcemanager.v2.organization.created", visibility, &traceParent, &traceState) }) } func validateSentEvent( t *testing.T, topicPrefix string, message *amqp.Message, objectIdentifier *auditV1.ObjectIdentifier, event *auditV1.AuditLogEntry, operationName string, visibility auditV1.Visibility, traceParent *string, traceState *string, ) { // Check topic name assert.Equal(t, fmt.Sprintf("topic://%s/%s", topicPrefix, objectIdentifier.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.ProtoPayload.RequestMetadata.RequestAttributes.Time.AsTime().UnixMilli(), applicationProperties["cloudEvents:time"]) assert.Equal(t, ContentTypeCloudEventsProtobuf, applicationProperties["cloudEvents:datacontenttype"]) assert.Equal(t, "audit.v1.RoutableAuditEvent", applicationProperties["cloudEvents:type"]) assert.Equal(t, *traceParent, applicationProperties["cloudEvents:traceparent"]) assert.Equal(t, *traceState, applicationProperties["cloudEvents:tracestate"]) // Check deserialized message validateRoutableEventPayload( t, message.Data[0], objectIdentifier, event, operationName, visibility) } func validateRoutableEventPayload( t *testing.T, payload []byte, objectIdentifier *auditV1.ObjectIdentifier, event *auditV1.AuditLogEntry, operationName string, visibility auditV1.Visibility, ) { // Check routable audit event parameters var routableAuditEvent auditV1.RoutableAuditEvent assert.NoError(t, proto.Unmarshal(payload, &routableAuditEvent)) assert.Equal(t, operationName, routableAuditEvent.OperationName) assert.Equal(t, visibility, routableAuditEvent.Visibility) assert.True(t, proto.Equal(objectIdentifier, routableAuditEvent.ObjectIdentifier)) var auditEvent auditV1.AuditLogEntry 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(NewRoutableIdentifier(&auditV1.ObjectIdentifier{Type: "unsupported"})) 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, RoutableSystemIdentifier) 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.LogWithTrace(context.Background(), event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier, 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, RoutableSystemIdentifier) assert.ErrorIs(t, err, ErrEventNil) }