diff --git a/core/notification/mocks/receiver_service.go b/core/notification/mocks/receiver_service.go index 10199362..297fd1d6 100644 --- a/core/notification/mocks/receiver_service.go +++ b/core/notification/mocks/receiver_service.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. +// Code generated by mockery v2.42.3. DO NOT EDIT. package mocks @@ -23,6 +23,80 @@ func (_m *ReceiverService) EXPECT() *ReceiverService_Expecter { return &ReceiverService_Expecter{mock: &_m.Mock} } +// Get provides a mock function with given fields: ctx, id, opts +func (_m *ReceiverService) Get(ctx context.Context, id uint64, opts ...receiver.GetOption) (*receiver.Receiver, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, id) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *receiver.Receiver + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, ...receiver.GetOption) (*receiver.Receiver, error)); ok { + return rf(ctx, id, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, ...receiver.GetOption) *receiver.Receiver); ok { + r0 = rf(ctx, id, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*receiver.Receiver) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, ...receiver.GetOption) error); ok { + r1 = rf(ctx, id, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ReceiverService_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type ReceiverService_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - id uint64 +// - opts ...receiver.GetOption +func (_e *ReceiverService_Expecter) Get(ctx interface{}, id interface{}, opts ...interface{}) *ReceiverService_Get_Call { + return &ReceiverService_Get_Call{Call: _e.mock.On("Get", + append([]interface{}{ctx, id}, opts...)...)} +} + +func (_c *ReceiverService_Get_Call) Run(run func(ctx context.Context, id uint64, opts ...receiver.GetOption)) *ReceiverService_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]receiver.GetOption, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(receiver.GetOption) + } + } + run(args[0].(context.Context), args[1].(uint64), variadicArgs...) + }) + return _c +} + +func (_c *ReceiverService_Get_Call) Return(_a0 *receiver.Receiver, _a1 error) *ReceiverService_Get_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ReceiverService_Get_Call) RunAndReturn(run func(context.Context, uint64, ...receiver.GetOption) (*receiver.Receiver, error)) *ReceiverService_Get_Call { + _c.Call.Return(run) + return _c +} + // List provides a mock function with given fields: ctx, flt func (_m *ReceiverService) List(ctx context.Context, flt receiver.Filter) ([]receiver.Receiver, error) { ret := _m.Called(ctx, flt) diff --git a/core/notification/notification.go b/core/notification/notification.go index c11153b4..3fc820e3 100644 --- a/core/notification/notification.go +++ b/core/notification/notification.go @@ -36,16 +36,16 @@ type Transactor interface { // Notification is a model of notification type Notification struct { - ID string `json:"id"` - NamespaceID uint64 `json:"namespace_id"` - Type string `json:"type"` - Data map[string]any `json:"data"` - Labels map[string]string `json:"labels"` - ValidDuration time.Duration `json:"valid_duration"` - Template string `json:"template"` - UniqueKey string `json:"unique_key"` - ReceiverSelectors []map[string]string `json:"receiver_selectors"` - CreatedAt time.Time `json:"created_at"` + ID string `json:"id"` + NamespaceID uint64 `json:"namespace_id"` + Type string `json:"type"` + Data map[string]any `json:"data"` + Labels map[string]string `json:"labels"` + ValidDuration time.Duration `json:"valid_duration"` + Template string `json:"template"` + UniqueKey string `json:"unique_key"` + ReceiverSelectors ReceiverSelectors `json:"receiver_selectors"` + CreatedAt time.Time `json:"created_at"` // won't be stored in notification table, only to propagate this to notification_subscriber AlertIDs []int64 diff --git a/core/notification/notification_test.go b/core/notification/notification_test.go index 6da50a90..5e355673 100644 --- a/core/notification/notification_test.go +++ b/core/notification/notification_test.go @@ -41,7 +41,7 @@ func TestNotification_Validate(t *testing.T) { Labels: map[string]string{ "receiver_id": "2", }, - ReceiverSelectors: []map[string]string{ + ReceiverSelectors: []map[string]any{ { "varkey1": "value1", }, diff --git a/core/notification/receiver_selectors.go b/core/notification/receiver_selectors.go new file mode 100644 index 00000000..36aaee41 --- /dev/null +++ b/core/notification/receiver_selectors.go @@ -0,0 +1,42 @@ +package notification + +import ( + "github.com/goto/siren/pkg/errors" +) + +type ReceiverSelectors []map[string]any + +func (rs ReceiverSelectors) parseAndValidate() ([]map[string]string, map[string]any, error) { + // Check if any selector contains a config + var selectorConfig map[string]any + for i := 0; i < len(rs); i++ { + selector := rs[i] + if v, cok := selector["config"]; cok { + if m, ok := v.(map[string]any); ok { + selectorConfig = m + delete(rs[i], "config") + } else { + return nil, nil, errors.ErrInvalid.WithMsgf("config should be in map and follow notification config") + } + break + } + } + + if selectorConfig != nil && len(rs) > 1 { + return nil, nil, errors.ErrInvalid.WithMsgf("config override could only be used with one selector") + } + + castedSelectors := make([]map[string]string, len(rs)) + for i, selector := range rs { + castedSelectors[i] = make(map[string]string) + for k, v := range selector { + if str, ok := v.(string); ok { + castedSelectors[i][k] = str + } else { + return nil, nil, errors.ErrInvalid.WithMsgf("receiver selector value of '%s' should be a string", k) + } + } + } + + return castedSelectors, selectorConfig, nil +} diff --git a/core/notification/router_receiver_service.go b/core/notification/router_receiver_service.go index b129df1b..1c138b55 100644 --- a/core/notification/router_receiver_service.go +++ b/core/notification/router_receiver_service.go @@ -26,8 +26,13 @@ func (s *RouterReceiverService) PrepareMetaMessages(ctx context.Context, n Notif return nil, nil, errors.ErrInvalid.WithMsgf("number of receiver selectors should be less than or equal threshold %d", s.deps.Cfg.MaxNumReceiverSelectors) } + selectors, selectorConfig, err := n.ReceiverSelectors.parseAndValidate() + if err != nil { + return nil, nil, err + } + rcvs, err := s.deps.ReceiverService.List(ctx, receiver.Filter{ - MultipleLabels: n.ReceiverSelectors, + MultipleLabels: selectors, Expanded: true, }) if err != nil { @@ -38,6 +43,24 @@ func (s *RouterReceiverService) PrepareMetaMessages(ctx context.Context, n Notif return nil, nil, errors.ErrNotFound } + if selectorConfig != nil && len(rcvs) > 1 { + return nil, nil, errors.ErrInvalid.WithMsgf("config override could only be used to 1 receiver, but got %d receiver", len(rcvs)) + } else if selectorConfig != nil && len(rcvs) == 1 { + // config override flow + var rcvView = &subscription.ReceiverView{} + rcvView.FromReceiver(rcvs[0]) + rcvView.Configurations = s.mergeReceiverConfig(rcvView.Configurations, selectorConfig) + metaMessages = append(metaMessages, n.MetaMessage(*rcvView)) + + notificationLogs = append(notificationLogs, log.Notification{ + NamespaceID: n.NamespaceID, + NotificationID: n.ID, + ReceiverID: rcvs[0].ID, + AlertIDs: n.AlertIDs, + }) + return metaMessages, notificationLogs, nil + } + for _, rcv := range rcvs { var rcvView = &subscription.ReceiverView{} rcvView.FromReceiver(rcv) @@ -58,3 +81,15 @@ func (s *RouterReceiverService) PrepareMetaMessages(ctx context.Context, n Notif return metaMessages, notificationLogs, nil } + +func (s *RouterReceiverService) mergeReceiverConfig(receiverConfig, selectorConfig map[string]any) map[string]any { + // override the existing config with the one from API if there is config clash + result := map[string]any{} + for k, v := range receiverConfig { + result[k] = v + } + for k, v := range selectorConfig { + result[k] = v + } + return result +} diff --git a/core/notification/router_receiver_service_test.go b/core/notification/router_receiver_service_test.go index 60443c4c..0e0a873e 100644 --- a/core/notification/router_receiver_service_test.go +++ b/core/notification/router_receiver_service_test.go @@ -26,16 +26,33 @@ func TestRouterReceiverService_PrepareMetaMessage(t *testing.T) { { name: "should return error if number of receiver selector is more than threshold", n: notification.Notification{ - ReceiverSelectors: []map[string]string{ + ReceiverSelectors: []map[string]any{ { "k1": "v1", }, { "k2": "v2", }, + { + "k3": "v3", + }, }, }, - wantErrStr: "number of receiver selectors should be less than or equal threshold 1", + wantErrStr: "number of receiver selectors should be less than or equal threshold 2", + }, + { + name: "should return error if receiver selector value is not string", + n: notification.Notification{ + ReceiverSelectors: []map[string]any{ + { + "k1": map[string]any{}, + }, + { + "k2": "v2", + }, + }, + }, + wantErrStr: "receiver selector value of 'k1' should be a string", }, { name: "should return error if receiver service return error", @@ -71,30 +88,195 @@ func TestRouterReceiverService_PrepareMetaMessage(t *testing.T) { }, wantErrStr: "sending 3 messages exceed max messages receiver flow threshold 2. this will spam and broadcast to 3 channel. found 0 receiver selectors passed, you might want to check your receiver selectors configuration", }, + // Config Override + { + name: "should return error if receiver selectors is more than one but there is a config override", + n: notification.Notification{ + ReceiverSelectors: []map[string]any{ + { + "k1": "v1", + "config": map[string]any{ + "k3": "v3", + }, + }, + { + "k2": "v2", + }, + }, + }, + setup: func(rs *mocks.ReceiverService, n *mocks.Notifier) { + rs.EXPECT().List(mock.AnythingOfType("context.todoCtx"), mock.AnythingOfType("receiver.Filter")).Return([]receiver.Receiver{ + { + ID: 1, + }, + { + ID: 2, + }, + { + ID: 3, + }, + }, nil) + }, + wantErrStr: "config override could only be used with one selector", + }, + { + name: "should return error if config override i receiver selectors is not a map", + n: notification.Notification{ + ReceiverSelectors: []map[string]any{ + { + "k1": "v1", + "config": 123, + }, + { + "k2": "v2", + }, + }, + }, + setup: func(rs *mocks.ReceiverService, n *mocks.Notifier) { + rs.EXPECT().List(mock.AnythingOfType("context.todoCtx"), mock.AnythingOfType("receiver.Filter")).Return([]receiver.Receiver{ + { + ID: 1, + }, + { + ID: 2, + }, + { + ID: 3, + }, + }, nil) + }, + wantErrStr: "config should be in map and follow notification config", + }, + { + name: "should return error if config override is being used to more than 1 evaluated receivers", + n: notification.Notification{ + ReceiverSelectors: []map[string]any{ + { + "k1": "v1", + "config": map[string]any{ + "k3": "v3", + }, + }, + }, + }, + setup: func(rs *mocks.ReceiverService, n *mocks.Notifier) { + rs.EXPECT().List(mock.AnythingOfType("context.todoCtx"), mock.AnythingOfType("receiver.Filter")).Return([]receiver.Receiver{ + { + ID: 1, + }, + { + ID: 2, + }, + { + ID: 3, + }, + }, nil) + }, + wantErrStr: "config override could only be used to 1 receiver, but got 3 receiver", + }, { name: "should return no error if succeed", - n: notification.Notification{}, + n: notification.Notification{ + ID: "test-notification-id", + NamespaceID: 123, + }, setup: func(rs *mocks.ReceiverService, n *mocks.Notifier) { rs.EXPECT().List(mock.AnythingOfType("context.todoCtx"), mock.AnythingOfType("receiver.Filter")).Return([]receiver.Receiver{ { ID: 1, + Configurations: map[string]interface{}{ + "token": "token1", + "workspace": "workspace1", + "channel_name": "channel1", + }, }, { ID: 2, + Configurations: map[string]interface{}{ + "token": "token2", + "workspace": "workspace2", + "channel_name": "channel2", + }, }, }, nil) }, want: []notification.MetaMessage{ { - ReceiverID: 1, + ReceiverID: 1, + NotificationIDs: []string{"test-notification-id"}, + ReceiverConfigs: map[string]interface{}{ + "token": "token1", + "workspace": "workspace1", + "channel_name": "channel1", + }, + }, + { + ReceiverID: 2, + NotificationIDs: []string{"test-notification-id"}, + ReceiverConfigs: map[string]interface{}{ + "token": "token2", + "workspace": "workspace2", + "channel_name": "channel2", + }, + }, + }, + want1: []log.Notification{ + { + ReceiverID: 1, + NotificationID: "test-notification-id", + NamespaceID: 123, + }, + { + ReceiverID: 2, + NotificationID: "test-notification-id", + NamespaceID: 123, + }, + }, + }, + { + name: "should return no error if succeed with config override feature", + n: notification.Notification{ + ID: "test-notification-id", + ReceiverSelectors: []map[string]any{ + { + "k1": "v1", + "config": map[string]any{ + "channel_name": "channel1", + "channel_type": "xyz", + }, + }, }, + NamespaceID: 123, + }, + setup: func(rs *mocks.ReceiverService, n *mocks.Notifier) { + rs.EXPECT().List(mock.AnythingOfType("context.todoCtx"), mock.AnythingOfType("receiver.Filter")).Return([]receiver.Receiver{ + { + ID: 1, + Configurations: map[string]interface{}{ + "token": "token1", + "workspace": "workspace1", + }, + }, + }, nil) + }, + want: []notification.MetaMessage{ { - ReceiverID: 2, + ReceiverID: 1, + NotificationIDs: []string{"test-notification-id"}, + ReceiverConfigs: map[string]interface{}{ + "token": "token1", + "workspace": "workspace1", + "channel_name": "channel1", + "channel_type": "xyz", + }, }, }, want1: []log.Notification{ - {ReceiverID: 1}, - {ReceiverID: 2}, + { + ReceiverID: 1, + NotificationID: "test-notification-id", + NamespaceID: 123, + }, }, }, } @@ -108,7 +290,7 @@ func TestRouterReceiverService_PrepareMetaMessage(t *testing.T) { s := notification.NewRouterReceiverService( notification.Deps{ Cfg: notification.Config{ - MaxNumReceiverSelectors: 1, + MaxNumReceiverSelectors: 2, MaxMessagesReceiverFlow: 2, }, Logger: saltlog.NewNoop(), diff --git a/core/subscription/subscription.go b/core/subscription/subscription.go index 7f2b1984..1d8afad5 100644 --- a/core/subscription/subscription.go +++ b/core/subscription/subscription.go @@ -48,7 +48,6 @@ func (rcv *ReceiverView) FromReceiver(r receiver.Receiver) { rcv.ParentID = r.ParentID rcv.CreatedAt = r.CreatedAt rcv.UpdatedAt = r.UpdatedAt - } type Receiver struct { diff --git a/internal/api/v1beta1/notification.go b/internal/api/v1beta1/notification.go index 5ffa8bc6..b0e911ae 100644 --- a/internal/api/v1beta1/notification.go +++ b/internal/api/v1beta1/notification.go @@ -19,7 +19,11 @@ import ( const notificationAPIScope = "notification_api" -func (s *GRPCServer) validatePostNotificationPayload(receiverSelectors []map[string]string, labels map[string]string) error { +func (s *GRPCServer) validatePostNotificationPayload(receiverSelectors []map[string]any, labels map[string]string) error { + if len(receiverSelectors) == 0 && len(labels) == 0 { + return errors.ErrInvalid.WithMsgf("receivers or labels must be provided") + } + if len(receiverSelectors) > 0 && len(labels) > 0 { return errors.ErrInvalid.WithMsgf("receivers and labels cannot being used at the same time, should be used either one of them") } @@ -46,10 +50,15 @@ func (s *GRPCServer) PostNotification(ctx context.Context, req *sirenv1beta1.Pos } } - var receiverSelectors = []map[string]string{} + var receiverSelectors = []map[string]any{} for _, pbSelector := range req.GetReceivers() { - var mss = make(map[string]string) + var mss = make(map[string]any) for k, v := range pbSelector.AsMap() { + // skip if key is config + if k == "config" { + mss[k] = v + continue + } vString, ok := v.(string) if !ok { err := errors.ErrInvalid.WithMsgf("invalid receiver selectors, value must be string but found %v", v) diff --git a/internal/api/v1beta1/notification_test.go b/internal/api/v1beta1/notification_test.go index 892d8f82..72679a3b 100644 --- a/internal/api/v1beta1/notification_test.go +++ b/internal/api/v1beta1/notification_test.go @@ -25,12 +25,26 @@ func TestGRPCServer_PostNotification(t *testing.T) { testCases := []struct { name string idempotencyKey string + request *sirenv1beta1.PostNotificationRequest setup func(*mocks.NotificationService) + expectedID string errString string }{ + { + name: "should return invalid argument if no receivers or labels provided", + idempotencyKey: "test", + request: &sirenv1beta1.PostNotificationRequest{}, + setup: func(ns *mocks.NotificationService) { + ns.EXPECT().CheckIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return("", errors.ErrNotFound) + }, + errString: "rpc error: code = InvalidArgument desc = receivers or labels must be provided", + }, { name: "should return invalid argument if post notification return invalid argument", idempotencyKey: "test", + request: &sirenv1beta1.PostNotificationRequest{ + Labels: map[string]string{"key": "value"}, + }, setup: func(ns *mocks.NotificationService) { ns.EXPECT().CheckIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return("", errors.ErrNotFound) ns.EXPECT().Dispatch(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("[]notification.Notification")).Return(nil, errors.ErrInvalid) @@ -40,15 +54,21 @@ func TestGRPCServer_PostNotification(t *testing.T) { { name: "should return internal error if post notification return some error", idempotencyKey: "test", + request: &sirenv1beta1.PostNotificationRequest{ + Labels: map[string]string{"key": "value"}, + }, setup: func(ns *mocks.NotificationService) { ns.EXPECT().CheckIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return("", errors.ErrNotFound) - ns.EXPECT().Dispatch(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("[]notification.Notification")).Return(nil, errors.New("some error")) + ns.EXPECT().Dispatch(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("[]notification.Notification")).Return(nil, errors.New("some unexpected error")) }, errString: "rpc error: code = Internal desc = some unexpected error occurred", }, { - name: "should return invalid error if post notification return err no message", + name: "should return invalid error if post notification return err_no_message", idempotencyKey: "test", + request: &sirenv1beta1.PostNotificationRequest{ + Labels: map[string]string{"key": "value"}, + }, setup: func(ns *mocks.NotificationService) { ns.EXPECT().CheckIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return("", errors.ErrNotFound) ns.EXPECT().Dispatch(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("[]notification.Notification")).Return(nil, notification.ErrNoMessage) @@ -57,10 +77,15 @@ func TestGRPCServer_PostNotification(t *testing.T) { }, { name: "should return success if request is idempotent", - idempotencyKey: "test", + idempotencyKey: "test-idempotent", + request: &sirenv1beta1.PostNotificationRequest{ + Labels: map[string]string{"key": "value"}, + }, setup: func(ns *mocks.NotificationService) { - ns.EXPECT().CheckIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(notificationID, nil) + ns.EXPECT().CheckIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), "test-idempotent").Return(notificationID, nil) }, + expectedID: notificationID, + errString: "", }, { name: "should return error if idempotency checking return error", @@ -71,24 +96,33 @@ func TestGRPCServer_PostNotification(t *testing.T) { errString: "rpc error: code = Internal desc = some unexpected error occurred", }, { - name: "should return error if error inserting idempotency", - idempotencyKey: "test", - setup: func(ns *mocks.NotificationService) { - ns.EXPECT().CheckIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return("", errors.ErrNotFound) - ns.EXPECT().Dispatch(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("[]notification.Notification")).Return([]string{notificationID}, nil) - ns.EXPECT().InsertIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(errors.New("some error")) - }, - errString: "rpc error: code = Internal desc = some unexpected error occurred", - }, + name: "should return error if error inserting idempotency", + idempotencyKey: "test", + request: &sirenv1beta1.PostNotificationRequest{ + Labels: map[string]string{"key": "value"}, + }, + setup: func(ns *mocks.NotificationService) { + ns.EXPECT().CheckIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return("", errors.ErrNotFound) + ns.EXPECT().Dispatch(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("[]notification.Notification")).Return([]string{notificationID}, nil) + ns.EXPECT().InsertIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), notificationID).Return(errors.New("some error")) + }, + errString: "rpc error: code = Internal desc = some unexpected error occurred", + }, + { - name: "should return OK response if post notification succeed", - idempotencyKey: "test", - setup: func(ns *mocks.NotificationService) { - ns.EXPECT().CheckIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return("", errors.ErrNotFound) - ns.EXPECT().Dispatch(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("[]notification.Notification")).Return([]string{notificationID}, nil) - ns.EXPECT().InsertIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return(nil) - }, - }, + name: "should return OK response if post notification succeed", + idempotencyKey: "test", + request: &sirenv1beta1.PostNotificationRequest{ + Labels: map[string]string{"key": "value"}, + }, + setup: func(ns *mocks.NotificationService) { + ns.EXPECT().CheckIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), mock.AnythingOfType("string")).Return("", errors.ErrNotFound) + ns.EXPECT().Dispatch(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("[]notification.Notification")).Return([]string{notificationID}, nil) + ns.EXPECT().InsertIdempotency(mock.AnythingOfType("*context.valueCtx"), mock.AnythingOfType("string"), mock.AnythingOfType("string"), notificationID).Return(nil) + }, + expectedID: notificationID, + errString: "", + }, } for _, tc := range testCases { @@ -109,10 +143,19 @@ func TestGRPCServer_PostNotification(t *testing.T) { ctx := metadata.NewIncomingContext(context.TODO(), metadata.New(map[string]string{ idempotencyHeaderKey: tc.idempotencyKey, })) - _, err = dummyGRPCServer.PostNotification(ctx, &sirenv1beta1.PostNotificationRequest{}) + resp, err := dummyGRPCServer.PostNotification(ctx, tc.request) - if (err != nil) && tc.errString != err.Error() { - t.Errorf("PostNotification() error = %v, wantErr %v", err, tc.errString) + if tc.errString != "" { + if err == nil || err.Error() != tc.errString { + t.Errorf("PostNotification() error = %v, wantErr %v", err, tc.errString) + } + } else { + if err != nil { + t.Errorf("PostNotification() unexpected error = %v", err) + } + if resp == nil || resp.NotificationId != tc.expectedID { + t.Errorf("PostNotification() got notification ID = %v, want %v", resp.GetNotificationId(), tc.expectedID) + } } mockNotificationService.AssertExpectations(t) @@ -133,7 +176,7 @@ func TestGRPCServer_ListNotifications(t *testing.T) { "data-key": "data-value", }, Labels: map[string]string{}, - ReceiverSelectors: []map[string]string{}, + ReceiverSelectors: []map[string]any{}, }, } diff --git a/internal/store/model/notification.go b/internal/store/model/notification.go index d1c4f03f..75f5815b 100644 --- a/internal/store/model/notification.go +++ b/internal/store/model/notification.go @@ -9,16 +9,16 @@ import ( ) type Notification struct { - ID string `db:"id"` - NamespaceID sql.NullInt64 `db:"namespace_id"` - Type string `db:"type"` - Data pgc.StringAnyMap `db:"data"` - Labels pgc.StringStringMap `db:"labels"` - ValidDuration pgc.TimeDuration `db:"valid_duration"` - UniqueKey sql.NullString `db:"unique_key"` - Template sql.NullString `db:"template"` - CreatedAt time.Time `db:"created_at"` - ReceiverSelectors pgc.ListStringStringMap `db:"receiver_selectors"` + ID string `db:"id"` + NamespaceID sql.NullInt64 `db:"namespace_id"` + Type string `db:"type"` + Data pgc.StringAnyMap `db:"data"` + Labels pgc.StringStringMap `db:"labels"` + ValidDuration pgc.TimeDuration `db:"valid_duration"` + UniqueKey sql.NullString `db:"unique_key"` + Template sql.NullString `db:"template"` + CreatedAt time.Time `db:"created_at"` + ReceiverSelectors pgc.ListStringAnyMap `db:"receiver_selectors"` } func (n *Notification) FromDomain(d notification.Notification) { @@ -47,10 +47,14 @@ func (n *Notification) FromDomain(d notification.Notification) { } n.CreatedAt = d.CreatedAt - n.ReceiverSelectors = d.ReceiverSelectors + n.ReceiverSelectors = pgc.ListStringAnyMap(d.ReceiverSelectors) } func (n *Notification) ToDomain() *notification.Notification { + var rcvSelectors notification.ReceiverSelectors + if len(n.ReceiverSelectors) > 0 { + rcvSelectors = []map[string]any(n.ReceiverSelectors) + } return ¬ification.Notification{ ID: n.ID, NamespaceID: uint64(n.NamespaceID.Int64), @@ -61,6 +65,6 @@ func (n *Notification) ToDomain() *notification.Notification { Template: n.Template.String, UniqueKey: n.UniqueKey.String, CreatedAt: n.CreatedAt, - ReceiverSelectors: n.ReceiverSelectors, + ReceiverSelectors: rcvSelectors, } } diff --git a/internal/store/postgres/notification.go b/internal/store/postgres/notification.go index fb66fabe..17638c46 100644 --- a/internal/store/postgres/notification.go +++ b/internal/store/postgres/notification.go @@ -146,8 +146,8 @@ func (r *NotificationRepository) List(ctx context.Context, flt notification.Filt return nil, err } - recieverSelectors := fmt.Sprintf("[" + string(rs) + "]") - matchReceiverSelectorExpression := sq.Expr("receiver_selectors @> ?", recieverSelectors) + receiverSelectors := fmt.Sprintf("[" + string(rs) + "]") + matchReceiverSelectorExpression := sq.Expr("receiver_selectors @> ?", receiverSelectors) queryBuilder = queryBuilder.Where(matchReceiverSelectorExpression) } diff --git a/internal/store/postgres/notification_test.go b/internal/store/postgres/notification_test.go index f209884d..a5b5b82d 100644 --- a/internal/store/postgres/notification_test.go +++ b/internal/store/postgres/notification_test.go @@ -227,7 +227,7 @@ func (s *NotificationRepositoryTestSuite) TestList() { "label-key": "label-value", }, Template: "", - ReceiverSelectors: []map[string]string{ + ReceiverSelectors: []map[string]any{ { "team": "gotocompany-infra", "severity": "WARNING", @@ -320,7 +320,7 @@ func (s *NotificationRepositoryTestSuite) TestList() { "label-key": "label-value", }, Template: "", - ReceiverSelectors: []map[string]string{ + ReceiverSelectors: []map[string]any{ { "team": "gotocompany-infra", "severity": "WARNING", diff --git a/pkg/pgc/type.go b/pkg/pgc/type.go index 25509497..e251d3cd 100644 --- a/pkg/pgc/type.go +++ b/pkg/pgc/type.go @@ -76,11 +76,11 @@ func (t TimeDuration) Value() (driver.Value, error) { return time.Duration(t).String(), nil } -type ListStringStringMap []map[string]string +type ListStringAnyMap []map[string]any -func (m *ListStringStringMap) Scan(value interface{}) error { +func (m *ListStringAnyMap) Scan(value interface{}) error { if value == nil { - m = new(ListStringStringMap) + m = new(ListStringAnyMap) return nil } b, ok := value.([]byte) @@ -90,7 +90,7 @@ func (m *ListStringStringMap) Scan(value interface{}) error { return json.Unmarshal(b, &m) } -func (a ListStringStringMap) Value() (driver.Value, error) { +func (a ListStringAnyMap) Value() (driver.Value, error) { if len(a) == 0 { return nil, nil }