package api import ( "context" "errors" "fmt" "strings" "testing" "time" "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/bufbuild/protovalidate-go" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "google.golang.org/protobuf/proto" ) 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) } type ProtobufValidatorMock struct { mock.Mock } func (m *ProtobufValidatorMock) Validate(msg proto.Message) error { args := m.Called(msg) return args.Error(0) } type TopicNameResolverMock struct { mock.Mock } func (m *TopicNameResolverMock) Resolve(routableIdentifier *RoutableIdentifier) (string, error) { args := m.Called(routableIdentifier) return args.String(0), args.Error(1) } func NewValidator(t *testing.T) ProtobufValidator { validator, err := protovalidate.New() var protoValidator 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, 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, NewRoutableIdentifier(objectIdentifier)) assert.EqualError(t, err, "validation error:\n - log_name: value is required [required]") } func Test_ValidateAndSerializePartially_RoutableEventValidationFailed(t *testing.T) { validator := NewValidator(t) event, objectIdentifier := newOrganizationAuditEvent(nil) _, err := validateAndSerializePartially(&validator, event, 3, NewRoutableIdentifier(objectIdentifier)) assert.EqualError(t, err, "validation error:\n - visibility: value must be one of the defined enum values [enum.defined_only]") } 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, 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, ErrObjectIdentifierNil) }) t.Run("Visibility public - object identifier system", func(t *testing.T) { _, err := validateAndSerializePartially( &validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier) assert.ErrorIs(t, err, ErrObjectIdentifierVisibilityMismatch) }) t.Run("Visibility public - object identifier set", func(t *testing.T) { routableEvent, err := validateAndSerializePartially( &validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, 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, RoutableSystemIdentifier) assert.ErrorIs(t, err, ErrAttributeIdentifierInvalid) }) t.Run("Visibility private - object identifier set", func(t *testing.T) { routableEvent, err := validateAndSerializePartially( &validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, 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, 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, ErrObjectIdentifierNil) }) t.Run("Visibility public - object identifier system", func(t *testing.T) { _, err := validateAndSerializePartially( &validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, RoutableSystemIdentifier) assert.ErrorIs(t, err, ErrObjectIdentifierVisibilityMismatch) }) t.Run("Visibility public - object identifier set", func(t *testing.T) { _, err := validateAndSerializePartially( &validator, event, auditV1.Visibility_VISIBILITY_PUBLIC, NewRoutableIdentifier( &auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(ObjectTypeOrganization)})) assert.ErrorIs(t, err, ErrInvalidRoutableIdentifierForSystemEvent) }) t.Run("Visibility private - object identifier system", func(t *testing.T) { routableEvent, err := validateAndSerializePartially( &validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, 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, NewRoutableIdentifier( &auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: string(ObjectTypeOrganization)})) assert.ErrorIs(t, err, 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, NewRoutableIdentifier(objectIdentifier)) assert.ErrorIs(t, err, 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, NewRoutableIdentifier(objectIdentifier)) assert.ErrorIs(t, err, 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, NewRoutableIdentifier(objectIdentifier)) assert.ErrorIs(t, err, 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, NewRoutableIdentifier(objectIdentifier)) assert.ErrorIs(t, err, 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, NewRoutableIdentifier(objectIdentifier)) assert.ErrorIs(t, err, ErrAttributeIdentifierInvalid) }) } func Test_ValidateAndSerializePartially_SystemEvent(t *testing.T) { validator := NewValidator(t) event := newSystemAuditEvent(nil) routableEvent, err := validateAndSerializePartially( &validator, event, auditV1.Visibility_VISIBILITY_PRIVATE, RoutableSystemIdentifier) assert.NoError(t, err) assert.Equal(t, event.LogName, fmt.Sprintf("system/%s/logs/%s", SystemIdentifier.Identifier, EventTypeSystemEvent)) assert.True(t, proto.Equal(routableEvent.ObjectIdentifier, SystemIdentifier)) } func Test_Send_TopicNameResolverNil(t *testing.T) { err := send(nil, nil, context.Background(), nil, nil) assert.ErrorIs(t, err, 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 TopicNameResolver = &topicNameResolverMock var cloudEvent = CloudEvent{} var messagingApi messaging.Api = &messaging.AmqpApi{} err := send(&topicNameResolver, &messagingApi, context.Background(), RoutableSystemIdentifier, &cloudEvent) assert.ErrorIs(t, err, expectedError) } func Test_Send_MessagingApiNil(t *testing.T) { var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"} err := send(&topicNameResolver, nil, context.Background(), nil, nil) assert.ErrorIs(t, err, ErrMessagingApiNil) } func Test_Send_CloudEventNil(t *testing.T) { var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"} var messagingApi messaging.Api = &messaging.AmqpApi{} err := send(&topicNameResolver, &messagingApi, context.Background(), nil, nil) assert.ErrorIs(t, err, ErrCloudEventNil) } func Test_Send_ObjectIdentifierNil(t *testing.T) { var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"} var messagingApi messaging.Api = &messaging.AmqpApi{} var cloudEvent = CloudEvent{} err := send(&topicNameResolver, &messagingApi, context.Background(), nil, &cloudEvent) assert.ErrorIs(t, err, ErrObjectIdentifierNil) } func Test_Send_UnsupportedObjectIdentifierType(t *testing.T) { var topicNameResolver TopicNameResolver = &LegacyTopicNameResolver{topicName: "test"} var messagingApi messaging.Api = &messaging.AmqpApi{} var cloudEvent = CloudEvent{} var objectIdentifier = auditV1.ObjectIdentifier{Identifier: uuid.NewString(), Type: "unsupported"} err := send(&topicNameResolver, &messagingApi, context.Background(), NewRoutableIdentifier(&objectIdentifier), &cloudEvent) assert.ErrorIs(t, err, ErrUnsupportedRoutableType) } func Test_Send(t *testing.T) { topicNameResolverMock := TopicNameResolverMock{} topicNameResolverMock.On("Resolve", mock.Anything).Return("topic", nil) var topicNameResolver TopicNameResolver = &topicNameResolverMock messagingApiMock := MessagingApiMock{} messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) var messagingApi messaging.Api = &messagingApiMock var cloudEvent = CloudEvent{} assert.NoError(t, send(&topicNameResolver, &messagingApi, context.Background(), 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 TopicNameResolver = &topicNameResolverMock messagingApiMock := MessagingApiMock{} messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) var messagingApi messaging.Api = &messagingApiMock traceState := "rojo=00f067aa0ba902b7,congo=t61rcWkgMzE" traceParent := "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" expectedTime := time.Now() var cloudEvent = CloudEvent{ SpecVersion: "1.0", Source: "resourcemanager", Id: "id", Time: expectedTime, DataContentType: ContentTypeCloudEventsProtobuf, DataType: "type", Subject: "subject", TraceParent: &traceParent, TraceState: &traceState, } assert.NoError(t, send(&topicNameResolver, &messagingApi, context.Background(), 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, 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, 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 TopicNameResolver = &topicNameResolverMock messagingApiMock := MessagingApiMock{} messagingApiMock.On("Send", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) var messagingApi messaging.Api = &messagingApiMock expectedTime := time.Now() var cloudEvent = CloudEvent{ SpecVersion: "1.0", Source: "resourcemanager", Id: "id", Time: expectedTime, DataContentType: ContentTypeCloudEventsProtobuf, DataType: "type", Subject: "subject", } assert.NoError(t, send(&topicNameResolver, &messagingApi, context.Background(), 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, 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, 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) }