package api import ( "context" "errors" "fmt" "strings" "testing" "time" "buf.build/go/protovalidate" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "google.golang.org/protobuf/proto" auditV1 "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/gen/go/audit/v1" pkgAuditCommon "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/audit/common" pkgMessagingApi "dev.azure.com/schwarzit/schwarzit.stackit-public/audit-go.git/pkg/messaging/api" ) type MessagingApiMock struct { mock.Mock } func (m *MessagingApiMock) Send( ctx context.Context, topic string, data []byte, contentType string, applicationProperties map[string]any, ) error { args := m.Called(ctx, topic, data, contentType, applicationProperties) return args.Error(0) } func (m *MessagingApiMock) Close(_ context.Context) error { return nil } type TopicNameResolverMock struct { mock.Mock } func (m *TopicNameResolverMock) Resolve(routableIdentifier *pkgAuditCommon.RoutableIdentifier) (string, error) { args := m.Called(routableIdentifier) return args.String(0), args.Error(1) } func NewValidator(t *testing.T) pkgAuditCommon.ProtobufValidator { validator, err := protovalidate.New() var protoValidator pkgAuditCommon.ProtobufValidator = validator assert.NoError(t, err) return protoValidator } func Test_ValidateAndSerializePartially_EventNil(t *testing.T) { validator := NewValidator(t) _, err := ValidateAndSerializePartially( validator, nil, auditV1.Visibility_VISIBILITY_PUBLIC, nil) assert.ErrorIs(t, err, pkgAuditCommon.ErrEventNil) } func Test_ValidateAndSerializePartially_AuditEventValidationFailed(t *testing.T) { validator := NewValidator(t) event, objectIdentifier := NewOrganizationAuditEvent(nil) event.LogName = "" _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)) assert.EqualError(t, err, "validation error: log_name: value is required") } func Test_ValidateAndSerializePartially_RoutableEventValidationFailed(t *testing.T) { validator := NewValidator(t) event, objectIdentifier := NewOrganizationAuditEvent(nil) _, err := ValidateAndSerializePartially(validator, event, 3, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)) assert.EqualError(t, err, "validation error: visibility: value must be one of the defined enum values") } func Test_ValidateAndSerializePartially_CheckVisibility_Event(t *testing.T) { validator := NewValidator(t) event, objectIdentifier := NewOrganizationAuditEvent(nil) t.Run("Visibility public - object identifier nil", func(t *testing.T) { _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, nil) assert.ErrorIs(t, err, pkgAuditCommon.ErrObjectIdentifierNil) }) t.Run("Visibility private - object identifier nil", func(t *testing.T) { _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil) assert.ErrorIs(t, err, pkgAuditCommon.ErrObjectIdentifierNil) }) t.Run("Visibility public - object identifier system", func(t *testing.T) { _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.RoutableSystemIdentifier) assert.ErrorIs(t, err, pkgAuditCommon.ErrObjectIdentifierVisibilityMismatch) }) t.Run("Visibility public - object identifier set", func(t *testing.T) { routableEvent, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)) assert.NoError(t, err) assert.NotNil(t, routableEvent) }) t.Run("Visibility private - object identifier system", func(t *testing.T) { _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, pkgAuditCommon.RoutableSystemIdentifier) assert.ErrorIs(t, err, pkgAuditCommon.ErrAttributeIdentifierInvalid) }) t.Run("Visibility private - object identifier set", func(t *testing.T) { routableEvent, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)) assert.NoError(t, err) assert.NotNil(t, routableEvent) }) } func Test_ValidateAndSerializePartially_CheckVisibility_SystemEvent(t *testing.T) { validator := NewValidator(t) event := NewSystemAuditEvent(nil) t.Run("Visibility public - object identifier nil", func(t *testing.T) { _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, nil) assert.ErrorIs(t, err, pkgAuditCommon.ErrObjectIdentifierNil) }) t.Run("Visibility private - object identifier nil", func(t *testing.T) { _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, nil) assert.ErrorIs(t, err, pkgAuditCommon.ErrObjectIdentifierNil) }) t.Run("Visibility public - object identifier system", func(t *testing.T) { _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.RoutableSystemIdentifier) assert.ErrorIs(t, err, pkgAuditCommon.ErrObjectIdentifierVisibilityMismatch) }) t.Run("Visibility public - object identifier set", func(t *testing.T) { _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.NewRoutableIdentifier( &auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(pkgAuditCommon.ObjectTypeOrganization)})) assert.ErrorIs(t, err, pkgAuditCommon.ErrInvalidRoutableIdentifierForSystemEvent) }) t.Run("Visibility private - object identifier system", func(t *testing.T) { routableEvent, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, pkgAuditCommon.RoutableSystemIdentifier) assert.NoError(t, err) assert.NotNil(t, routableEvent) }) t.Run("Visibility private - object identifier set", func(t *testing.T) { _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, pkgAuditCommon.NewRoutableIdentifier( &auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(pkgAuditCommon.ObjectTypeOrganization)})) assert.ErrorIs(t, err, pkgAuditCommon.ErrInvalidRoutableIdentifierForSystemEvent) }) } func Test_ValidateAndSerializePartially_UnsupportedIdentifierType(t *testing.T) { validator := NewValidator(t) event, objectIdentifier := NewFolderAuditEvent(nil) objectIdentifier.Type = "invalid" _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)) assert.ErrorIs(t, err, pkgAuditCommon.ErrUnsupportedRoutableType) } func Test_ValidateAndSerializePartially_LogNameIdentifierMismatch(t *testing.T) { validator := NewValidator(t) event, objectIdentifier := NewFolderAuditEvent(nil) parts := strings.Split(event.LogName, "/") identifier := parts[1] t.Run("LogName type mismatch", func(t *testing.T) { event.LogName = fmt.Sprintf("projects/%s/logs/admin-activity", identifier) _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)) assert.ErrorIs(t, err, pkgAuditCommon.ErrAttributeTypeInvalid) }) t.Run("LogName identifier mismatch", func(t *testing.T) { event.LogName = fmt.Sprintf("folders/%s/logs/admin-activity", uuid.NewString()) _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)) assert.ErrorIs(t, err, pkgAuditCommon.ErrAttributeIdentifierInvalid) }) } func Test_ValidateAndSerializePartially_ResourceNameIdentifierMismatch(t *testing.T) { validator := NewValidator(t) event, objectIdentifier := NewFolderAuditEvent(nil) parts := strings.Split(event.ProtoPayload.ResourceName, "/") identifier := parts[1] t.Run("ResourceName type mismatch", func(t *testing.T) { event.ProtoPayload.ResourceName = fmt.Sprintf("projects/%s", identifier) _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)) assert.ErrorIs(t, err, pkgAuditCommon.ErrAttributeTypeInvalid) }) t.Run("ResourceName identifier mismatch", func(t *testing.T) { event.ProtoPayload.ResourceName = fmt.Sprintf("folders/%s", uuid.NewString()) _, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, pkgAuditCommon.NewRoutableIdentifier(objectIdentifier)) assert.ErrorIs(t, err, pkgAuditCommon.ErrAttributeIdentifierInvalid) }) } func Test_ValidateAndSerializePartially_SystemEvent(t *testing.T) { validator := NewValidator(t) event := NewSystemAuditEvent(nil) routableEvent, err := ValidateAndSerializePartially( validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, pkgAuditCommon.RoutableSystemIdentifier) assert.NoError(t, err) assert.Equal(t, event.LogName, fmt.Sprintf("system/%s/logs/%s", pkgAuditCommon.SystemIdentifier.Identifier, pkgAuditCommon.EventTypeSystemEvent)) assert.True(t, proto.Equal(routableEvent.ObjectIdentifier, pkgAuditCommon.SystemIdentifier)) } func Test_Send_TopicNameResolverNil(t *testing.T) { err := Send(nil, nil, context.Background(), nil, nil) assert.ErrorIs(t, err, pkgAuditCommon.ErrTopicNameResolverNil) } func Test_Send_TopicNameResolutionError(t *testing.T) { expectedError := errors.New("expected error") topicNameResolverMock := TopicNameResolverMock{} topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", expectedError) var topicNameResolver pkgAuditCommon.TopicNameResolver = &topicNameResolverMock var cloudEvent = pkgAuditCommon.CloudEvent{} var messagingApi pkgMessagingApi.Api = &pkgMessagingApi.AmqpApi{} err := Send(topicNameResolver, messagingApi, context.Background(), pkgAuditCommon.RoutableSystemIdentifier, &cloudEvent) assert.ErrorIs(t, err, expectedError) } func Test_Send_MessagingApiNil(t *testing.T) { var topicNameResolver pkgAuditCommon.TopicNameResolver = &pkgAuditCommon.StaticTopicNameTestResolver{TopicName: "test"} err := Send(topicNameResolver, nil, context.Background(), nil, nil) assert.ErrorIs(t, err, pkgAuditCommon.ErrMessagingApiNil) } func Test_Send_CloudEventNil(t *testing.T) { var topicNameResolver pkgAuditCommon.TopicNameResolver = &pkgAuditCommon.StaticTopicNameTestResolver{TopicName: "test"} var messagingApi pkgMessagingApi.Api = &pkgMessagingApi.AmqpApi{} err := Send(topicNameResolver, messagingApi, context.Background(), nil, nil) assert.ErrorIs(t, err, pkgAuditCommon.ErrCloudEventNil) } func Test_Send_ObjectIdentifierNil(t *testing.T) { var topicNameResolver pkgAuditCommon.TopicNameResolver = &pkgAuditCommon.StaticTopicNameTestResolver{TopicName: "test"} var messagingApi pkgMessagingApi.Api = &pkgMessagingApi.AmqpApi{} var cloudEvent = pkgAuditCommon.CloudEvent{} err := Send(topicNameResolver, messagingApi, context.Background(), nil, &cloudEvent) assert.ErrorIs(t, err, pkgAuditCommon.ErrObjectIdentifierNil) } func Test_Send_UnsupportedObjectIdentifierType(t *testing.T) { var topicNameResolver pkgAuditCommon.TopicNameResolver = &pkgAuditCommon.StaticTopicNameTestResolver{TopicName: "test"} var messagingApi pkgMessagingApi.Api = &pkgMessagingApi.AmqpApi{} var cloudEvent = pkgAuditCommon.CloudEvent{} var objectIdentifier = auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: "unsupported"} err := Send(topicNameResolver, messagingApi, context.Background(), pkgAuditCommon.NewRoutableIdentifier(&objectIdentifier), &cloudEvent) assert.ErrorIs(t, err, pkgAuditCommon.ErrUnsupportedRoutableType) } func Test_Send(t *testing.T) { topicNameResolverMock := TopicNameResolverMock{} topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", nil) var topicNameResolver pkgAuditCommon.TopicNameResolver = &topicNameResolverMock messagingApiMock := MessagingApiMock{} messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) var messagingApi pkgMessagingApi.Api = &messagingApiMock var cloudEvent = pkgAuditCommon.CloudEvent{} assert.NoError(t, Send(topicNameResolver, messagingApi, context.Background(), pkgAuditCommon.RoutableSystemIdentifier, &cloudEvent)) assert.True(t, messagingApiMock.AssertNumberOfCalls(t, "Send", 1)) } func Test_SendAllHeadersSet(t *testing.T) { topicNameResolverMock := TopicNameResolverMock{} topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", nil) var topicNameResolver pkgAuditCommon.TopicNameResolver = &topicNameResolverMock messagingApiMock := MessagingApiMock{} messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) var messagingApi pkgMessagingApi.Api = &messagingApiMock traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE" traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" expectedTime := time.Now() var cloudEvent = pkgAuditCommon.CloudEvent{ SpecVersion: "1.0", Source: "resourcemanager", Id: "id", Time: expectedTime, DataContentType: pkgAuditCommon.ContentTypeCloudEventsProtobuf, DataType: "type", Subject: "subject", TraceParent: &traceParent, TraceState: &traceState, } assert.NoError(t, Send(topicNameResolver, messagingApi, context.Background(), pkgAuditCommon.RoutableSystemIdentifier, &cloudEvent)) assert.True(t, messagingApiMock.AssertNumberOfCalls(t, "Send", 1)) arguments := messagingApiMock.Calls[0].Arguments topic := arguments.Get(1).(string) assert.Equal(t, "topic", topic) contentType := arguments.Get(3).(string) assert.Equal(t, pkgAuditCommon.ContentTypeCloudEventsProtobuf, contentType) applicationProperties := arguments.Get(4).(map[string]any) assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"]) assert.Equal(t, "resourcemanager", applicationProperties["cloudEvents:source"]) assert.Equal(t, "id", applicationProperties["cloudEvents:id"]) assert.Equal(t, expectedTime.UnixMilli(), applicationProperties["cloudEvents:time"]) assert.Equal(t, pkgAuditCommon.ContentTypeCloudEventsProtobuf, applicationProperties["cloudEvents:datacontenttype"]) assert.Equal(t, "type", applicationProperties["cloudEvents:type"]) assert.Equal(t, "subject", applicationProperties["cloudEvents:subject"]) assert.Equal(t, traceParent, applicationProperties["cloudEvents:traceparent"]) assert.Equal(t, traceState, applicationProperties["cloudEvents:tracestate"]) messagingApiMock.AssertExpectations(t) } func Test_SendWithoutOptionalHeadersSet(t *testing.T) { topicNameResolverMock := TopicNameResolverMock{} topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", nil) var topicNameResolver pkgAuditCommon.TopicNameResolver = &topicNameResolverMock messagingApiMock := MessagingApiMock{} messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) var messagingApi pkgMessagingApi.Api = &messagingApiMock expectedTime := time.Now() var cloudEvent = pkgAuditCommon.CloudEvent{ SpecVersion: "1.0", Source: "resourcemanager", Id: "id", Time: expectedTime, DataContentType: pkgAuditCommon.ContentTypeCloudEventsProtobuf, DataType: "type", Subject: "subject", } assert.NoError(t, Send(topicNameResolver, messagingApi, context.Background(), pkgAuditCommon.RoutableSystemIdentifier, &cloudEvent)) assert.True(t, messagingApiMock.AssertNumberOfCalls(t, "Send", 1)) arguments := messagingApiMock.Calls[0].Arguments topic := arguments.Get(1).(string) assert.Equal(t, "topic", topic) contentType := arguments.Get(3).(string) assert.Equal(t, pkgAuditCommon.ContentTypeCloudEventsProtobuf, contentType) applicationProperties := arguments.Get(4).(map[string]any) assert.Equal(t, "1.0", applicationProperties["cloudEvents:specversion"]) assert.Equal(t, "resourcemanager", applicationProperties["cloudEvents:source"]) assert.Equal(t, "id", applicationProperties["cloudEvents:id"]) assert.Equal(t, expectedTime.UnixMilli(), applicationProperties["cloudEvents:time"]) assert.Equal(t, pkgAuditCommon.ContentTypeCloudEventsProtobuf, applicationProperties["cloudEvents:datacontenttype"]) assert.Equal(t, "type", applicationProperties["cloudEvents:type"]) assert.Equal(t, "subject", applicationProperties["cloudEvents:subject"]) assert.Equal(t, nil, applicationProperties["cloudEvents:traceparent"]) assert.Equal(t, nil, applicationProperties["cloudEvents:tracestate"]) messagingApiMock.AssertExpectations(t) } func Test_ValidateTopicNames(t *testing.T) { t.Run("conway", func(t *testing.T) { topicName := "topic://stackit-platform/t/swz/audit-log/conway/v1.0/service-name/events" assert.True(t, TopicNamePattern.MatchString(topicName)) }) t.Run("eu01", func(t *testing.T) { topicName := "topic://stackit-platform/t/swz/audit-log/eu01/v1.0/service-name/events" assert.True(t, TopicNamePattern.MatchString(topicName)) }) t.Run("eu02", func(t *testing.T) { topicName := "topic://stackit-platform/t/swz/audit-log/eu02/v1.0/service-name/events" assert.True(t, TopicNamePattern.MatchString(topicName)) }) t.Run("sx-stoi01", func(t *testing.T) { topicName := "topic://stackit-platform/t/swz/audit-log/sx-stoi01/v1.0/service-name/events" assert.True(t, TopicNamePattern.MatchString(topicName)) }) t.Run("version without decimals", func(t *testing.T) { topicName := "topic://stackit-platform/t/swz/audit-log/eu01/v1/service-name/events" assert.True(t, TopicNamePattern.MatchString(topicName)) }) t.Run("version as uppercase", func(t *testing.T) { topicName := "topic://stackit-platform/t/swz/audit-log/eu01/V1.0/service-name/events" assert.True(t, TopicNamePattern.MatchString(topicName)) }) t.Run("service name without dash", func(t *testing.T) { topicName := "topic://stackit-platform/t/swz/audit-log/eu01/v1.0/service/events" assert.True(t, TopicNamePattern.MatchString(topicName)) }) t.Run("multiple additional parts", func(t *testing.T) { topicName := "topic://stackit-platform/t/swz/audit-log/eu01/v1.0/service-name/multiple/additional/parts" assert.True(t, TopicNamePattern.MatchString(topicName)) }) t.Run("additional parts with dash", func(t *testing.T) { topicName := "topic://stackit-platform/t/swz/audit-log/eu01/v1.0/service-name/multiple-additional/parts" assert.True(t, TopicNamePattern.MatchString(topicName)) }) t.Run("topic prefix missing", func(t *testing.T) { topicName := "stackit-platform/t/swz/audit-log/eu01/v1.0/service-name/events" assert.False(t, TopicNamePattern.MatchString(topicName)) }) t.Run("invalid region", func(t *testing.T) { topicName := "topic://stackit-platform/t/swz/audit-log/invalid/v1.0/service-name/events" assert.False(t, TopicNamePattern.MatchString(topicName)) }) t.Run("additional parts missing", func(t *testing.T) { topicName := "topic://stackit-platform/t/swz/audit-log/eu01/v1.0/service-name" assert.False(t, TopicNamePattern.MatchString(topicName)) }) }