diff --git a/Makefile b/Makefile index c83bf5730..d792190aa 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ COMMIT := $(shell git rev-parse --short HEAD) TAG := "$(shell git rev-list --tags --max-count=1)" VERSION := "$(shell git describe --tags ${TAG})-next" BUILD_DIR=dist -PROTON_COMMIT := "869341885f6b35fc3b647987958c8d2feb9a459d" +PROTON_COMMIT := "07884ade7983c5de0fde7d426f23574d82ce31a6" .PHONY: all build clean test tidy vet proto setup format generate diff --git a/api/handler/v1beta1/approval.go b/api/handler/v1beta1/approval.go index 1fd4fd5a8..f1c39ecb7 100644 --- a/api/handler/v1beta1/approval.go +++ b/api/handler/v1beta1/approval.go @@ -7,6 +7,7 @@ import ( guardianv1beta1 "github.com/goto/guardian/api/proto/gotocompany/guardian/v1beta1" "github.com/goto/guardian/core/appeal" "github.com/goto/guardian/domain" + "golang.org/x/sync/errgroup" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) @@ -17,8 +18,11 @@ func (s *GRPCServer) ListUserApprovals(ctx context.Context, req *guardianv1beta1 return nil, status.Error(codes.Unauthenticated, err.Error()) } - approvals, err := s.listApprovals(ctx, &domain.ListApprovalsFilter{ + approvals, total, err := s.listApprovals(ctx, &domain.ListApprovalsFilter{ + Q: req.GetQ(), AccountID: req.GetAccountId(), + AccountTypes: req.GetAccountTypes(), + ResourceTypes: req.GetResourceTypes(), CreatedBy: user, Statuses: req.GetStatuses(), OrderBy: req.GetOrderBy(), @@ -32,12 +36,16 @@ func (s *GRPCServer) ListUserApprovals(ctx context.Context, req *guardianv1beta1 return &guardianv1beta1.ListUserApprovalsResponse{ Approvals: approvals, + Total: int32(total), }, nil } func (s *GRPCServer) ListApprovals(ctx context.Context, req *guardianv1beta1.ListApprovalsRequest) (*guardianv1beta1.ListApprovalsResponse, error) { - approvals, err := s.listApprovals(ctx, &domain.ListApprovalsFilter{ + approvals, total, err := s.listApprovals(ctx, &domain.ListApprovalsFilter{ + Q: req.GetQ(), AccountID: req.GetAccountId(), + AccountTypes: req.GetAccountTypes(), + ResourceTypes: req.GetResourceTypes(), CreatedBy: req.GetCreatedBy(), Statuses: req.GetStatuses(), OrderBy: req.GetOrderBy(), @@ -51,6 +59,7 @@ func (s *GRPCServer) ListApprovals(ctx context.Context, req *guardianv1beta1.Lis return &guardianv1beta1.ListApprovalsResponse{ Approvals: approvals, + Total: int32(total), }, nil } @@ -150,20 +159,41 @@ func (s *GRPCServer) DeleteApprover(ctx context.Context, req *guardianv1beta1.De }, nil } -func (s *GRPCServer) listApprovals(ctx context.Context, filters *domain.ListApprovalsFilter) ([]*guardianv1beta1.Approval, error) { - approvals, err := s.approvalService.ListApprovals(ctx, filters) - if err != nil { - return nil, status.Errorf(codes.Internal, "failed to get approval list: %s", err) +func (s *GRPCServer) listApprovals(ctx context.Context, filters *domain.ListApprovalsFilter) ([]*guardianv1beta1.Approval, int64, error) { + eg, ctx := errgroup.WithContext(ctx) + var approvals []*domain.Approval + var total int64 + + eg.Go(func() error { + approvalRecords, err := s.approvalService.ListApprovals(ctx, filters) + if err != nil { + return status.Errorf(codes.Internal, "failed to get approval list: %s", err) + } + approvals = approvalRecords + return nil + }) + + eg.Go(func() error { + totalRecord, err := s.approvalService.GetApprovalsTotalCount(ctx, filters) + if err != nil { + return status.Errorf(codes.Internal, "failed to get approval list: %v", err) + } + total = totalRecord + return nil + }) + + if err := eg.Wait(); err != nil { + return nil, 0, err } approvalProtos := []*guardianv1beta1.Approval{} for _, a := range approvals { approvalProto, err := s.adapter.ToApprovalProto(a) if err != nil { - return nil, status.Errorf(codes.Internal, "failed to parse approval: %v: %s", a.ID, err) + return nil, 0, status.Errorf(codes.Internal, "failed to parse approval: %v: %s", a.ID, err) } approvalProtos = append(approvalProtos, approvalProto) } - return approvalProtos, nil + return approvalProtos, total, nil } diff --git a/api/handler/v1beta1/approval_test.go b/api/handler/v1beta1/approval_test.go index 3cc700362..01fb2fdad 100644 --- a/api/handler/v1beta1/approval_test.go +++ b/api/handler/v1beta1/approval_test.go @@ -107,9 +107,12 @@ func (s *GrpcHandlersSuite) TestListUserApprovals() { }, }, }, + Total: 1, } - s.approvalService.EXPECT().ListApprovals(mock.AnythingOfType("*context.valueCtx"), expectedFilters). + s.approvalService.EXPECT().ListApprovals(mock.AnythingOfType("*context.cancelCtx"), expectedFilters). Return(expectedApprovals, nil).Once() + s.approvalService.EXPECT().GetApprovalsTotalCount(mock.AnythingOfType("*context.cancelCtx"), expectedFilters). + Return(int64(1), nil).Once() req := &guardianv1beta1.ListUserApprovalsRequest{ AccountId: "test-account-id", @@ -138,12 +141,32 @@ func (s *GrpcHandlersSuite) TestListUserApprovals() { s.approvalService.AssertExpectations(s.T()) }) - s.Run("should return internal error if approval service returns an error", func() { + s.Run("should return internal error if approvalService.ListApprovals returns an error", func() { s.setup() expectedError := errors.New("random error") - s.approvalService.EXPECT().ListApprovals(mock.AnythingOfType("*context.valueCtx"), mock.Anything). + s.approvalService.EXPECT().ListApprovals(mock.AnythingOfType("*context.cancelCtx"), mock.Anything). Return(nil, expectedError).Once() + s.approvalService.EXPECT().GetApprovalsTotalCount(mock.AnythingOfType("*context.cancelCtx"), mock.Anything). + Return(int64(0), nil).Once() + + req := &guardianv1beta1.ListUserApprovalsRequest{} + ctx := context.WithValue(context.Background(), authEmailTestContextKey{}, "test-user") + res, err := s.grpcServer.ListUserApprovals(ctx, req) + + s.Equal(codes.Internal, status.Code(err)) + s.Nil(res) + s.approvalService.AssertExpectations(s.T()) + }) + + s.Run("should return internal error if approvalService.GetApprovalsTotalCount returns an error", func() { + s.setup() + + s.approvalService.EXPECT().ListApprovals(mock.AnythingOfType("*context.cancelCtx"), mock.Anything). + Return([]*domain.Approval{}, nil).Once() + expectedError := errors.New("random error") + s.approvalService.EXPECT().GetApprovalsTotalCount(mock.AnythingOfType("*context.cancelCtx"), mock.Anything). + Return(int64(0), expectedError).Once() req := &guardianv1beta1.ListUserApprovalsRequest{} ctx := context.WithValue(context.Background(), authEmailTestContextKey{}, "test-user") @@ -166,8 +189,10 @@ func (s *GrpcHandlersSuite) TestListUserApprovals() { }, }, } - s.approvalService.EXPECT().ListApprovals(mock.AnythingOfType("*context.valueCtx"), mock.Anything). + s.approvalService.EXPECT().ListApprovals(mock.AnythingOfType("*context.cancelCtx"), mock.Anything). Return(invalidApprovals, nil).Once() + s.approvalService.EXPECT().GetApprovalsTotalCount(mock.AnythingOfType("*context.cancelCtx"), mock.Anything). + Return(int64(1), nil).Once() req := &guardianv1beta1.ListUserApprovalsRequest{} ctx := context.WithValue(context.Background(), authEmailTestContextKey{}, "test-user") @@ -268,9 +293,12 @@ func (s *GrpcHandlersSuite) TestListApprovals() { }, }, }, + Total: 1, } - s.approvalService.EXPECT().ListApprovals(mock.AnythingOfType("*context.emptyCtx"), expectedFilters). + s.approvalService.EXPECT().ListApprovals(mock.AnythingOfType("*context.cancelCtx"), expectedFilters). Return(expectedApprovals, nil).Once() + s.approvalService.EXPECT().GetApprovalsTotalCount(mock.AnythingOfType("*context.cancelCtx"), expectedFilters). + Return(int64(1), nil).Once() req := &guardianv1beta1.ListApprovalsRequest{ AccountId: "test-account-id", @@ -289,8 +317,10 @@ func (s *GrpcHandlersSuite) TestListApprovals() { s.setup() expectedError := errors.New("random error") - s.approvalService.EXPECT().ListApprovals(mock.AnythingOfType("*context.emptyCtx"), mock.Anything). + s.approvalService.EXPECT().ListApprovals(mock.AnythingOfType("*context.cancelCtx"), mock.Anything). Return(nil, expectedError).Once() + s.approvalService.EXPECT().GetApprovalsTotalCount(mock.AnythingOfType("*context.cancelCtx"), mock.Anything). + Return(int64(0), nil).Once() req := &guardianv1beta1.ListApprovalsRequest{} res, err := s.grpcServer.ListApprovals(context.Background(), req) @@ -312,8 +342,10 @@ func (s *GrpcHandlersSuite) TestListApprovals() { }, }, } - s.approvalService.EXPECT().ListApprovals(mock.AnythingOfType("*context.emptyCtx"), mock.Anything). + s.approvalService.EXPECT().ListApprovals(mock.AnythingOfType("*context.cancelCtx"), mock.Anything). Return(invalidApprovals, nil).Once() + s.approvalService.EXPECT().GetApprovalsTotalCount(mock.AnythingOfType("*context.cancelCtx"), mock.Anything). + Return(int64(1), nil).Once() req := &guardianv1beta1.ListApprovalsRequest{} res, err := s.grpcServer.ListApprovals(context.Background(), req) diff --git a/api/handler/v1beta1/grpc.go b/api/handler/v1beta1/grpc.go index 05bfe71e3..cd0123101 100644 --- a/api/handler/v1beta1/grpc.go +++ b/api/handler/v1beta1/grpc.go @@ -95,6 +95,7 @@ type appealService interface { //go:generate mockery --name=approvalService --exported --with-expecter type approvalService interface { ListApprovals(context.Context, *domain.ListApprovalsFilter) ([]*domain.Approval, error) + GetApprovalsTotalCount(context.Context, *domain.ListApprovalsFilter) (int64, error) BulkInsert(context.Context, []*domain.Approval) error } diff --git a/api/handler/v1beta1/mocks/approvalService.go b/api/handler/v1beta1/mocks/approvalService.go index 594ac80a4..71ad87757 100644 --- a/api/handler/v1beta1/mocks/approvalService.go +++ b/api/handler/v1beta1/mocks/approvalService.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks @@ -42,8 +42,8 @@ type ApprovalService_BulkInsert_Call struct { } // BulkInsert is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 []*domain.Approval +// - _a0 context.Context +// - _a1 []*domain.Approval func (_e *ApprovalService_Expecter) BulkInsert(_a0 interface{}, _a1 interface{}) *ApprovalService_BulkInsert_Call { return &ApprovalService_BulkInsert_Call{Call: _e.mock.On("BulkInsert", _a0, _a1)} } @@ -60,11 +60,73 @@ func (_c *ApprovalService_BulkInsert_Call) Return(_a0 error) *ApprovalService_Bu return _c } +func (_c *ApprovalService_BulkInsert_Call) RunAndReturn(run func(context.Context, []*domain.Approval) error) *ApprovalService_BulkInsert_Call { + _c.Call.Return(run) + return _c +} + +// GetApprovalsTotalCount provides a mock function with given fields: _a0, _a1 +func (_m *ApprovalService) GetApprovalsTotalCount(_a0 context.Context, _a1 *domain.ListApprovalsFilter) (int64, error) { + ret := _m.Called(_a0, _a1) + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *domain.ListApprovalsFilter) (int64, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *domain.ListApprovalsFilter) int64); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, *domain.ListApprovalsFilter) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ApprovalService_GetApprovalsTotalCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetApprovalsTotalCount' +type ApprovalService_GetApprovalsTotalCount_Call struct { + *mock.Call +} + +// GetApprovalsTotalCount is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *domain.ListApprovalsFilter +func (_e *ApprovalService_Expecter) GetApprovalsTotalCount(_a0 interface{}, _a1 interface{}) *ApprovalService_GetApprovalsTotalCount_Call { + return &ApprovalService_GetApprovalsTotalCount_Call{Call: _e.mock.On("GetApprovalsTotalCount", _a0, _a1)} +} + +func (_c *ApprovalService_GetApprovalsTotalCount_Call) Run(run func(_a0 context.Context, _a1 *domain.ListApprovalsFilter)) *ApprovalService_GetApprovalsTotalCount_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*domain.ListApprovalsFilter)) + }) + return _c +} + +func (_c *ApprovalService_GetApprovalsTotalCount_Call) Return(_a0 int64, _a1 error) *ApprovalService_GetApprovalsTotalCount_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ApprovalService_GetApprovalsTotalCount_Call) RunAndReturn(run func(context.Context, *domain.ListApprovalsFilter) (int64, error)) *ApprovalService_GetApprovalsTotalCount_Call { + _c.Call.Return(run) + return _c +} + // ListApprovals provides a mock function with given fields: _a0, _a1 func (_m *ApprovalService) ListApprovals(_a0 context.Context, _a1 *domain.ListApprovalsFilter) ([]*domain.Approval, error) { ret := _m.Called(_a0, _a1) var r0 []*domain.Approval + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *domain.ListApprovalsFilter) ([]*domain.Approval, error)); ok { + return rf(_a0, _a1) + } if rf, ok := ret.Get(0).(func(context.Context, *domain.ListApprovalsFilter) []*domain.Approval); ok { r0 = rf(_a0, _a1) } else { @@ -73,7 +135,6 @@ func (_m *ApprovalService) ListApprovals(_a0 context.Context, _a1 *domain.ListAp } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, *domain.ListApprovalsFilter) error); ok { r1 = rf(_a0, _a1) } else { @@ -89,8 +150,8 @@ type ApprovalService_ListApprovals_Call struct { } // ListApprovals is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 *domain.ListApprovalsFilter +// - _a0 context.Context +// - _a1 *domain.ListApprovalsFilter func (_e *ApprovalService_Expecter) ListApprovals(_a0 interface{}, _a1 interface{}) *ApprovalService_ListApprovals_Call { return &ApprovalService_ListApprovals_Call{Call: _e.mock.On("ListApprovals", _a0, _a1)} } @@ -107,6 +168,11 @@ func (_c *ApprovalService_ListApprovals_Call) Return(_a0 []*domain.Approval, _a1 return _c } +func (_c *ApprovalService_ListApprovals_Call) RunAndReturn(run func(context.Context, *domain.ListApprovalsFilter) ([]*domain.Approval, error)) *ApprovalService_ListApprovals_Call { + _c.Call.Return(run) + return _c +} + type mockConstructorTestingTNewApprovalService interface { mock.TestingT Cleanup(func()) diff --git a/api/proto/gotocompany/guardian/v1beta1/guardian.pb.go b/api/proto/gotocompany/guardian/v1beta1/guardian.pb.go index 4a4de6f2e..be0b2e572 100644 --- a/api/proto/gotocompany/guardian/v1beta1/guardian.pb.go +++ b/api/proto/gotocompany/guardian/v1beta1/guardian.pb.go @@ -2937,6 +2937,9 @@ type ListUserApprovalsRequest struct { Size uint32 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"` Offset uint32 `protobuf:"varint,6,opt,name=offset,proto3" json:"offset,omitempty"` AppealStatuses []string `protobuf:"bytes,7,rep,name=appeal_statuses,json=appealStatuses,proto3" json:"appeal_statuses,omitempty"` + Q string `protobuf:"bytes,8,opt,name=q,proto3" json:"q,omitempty"` + AccountTypes []string `protobuf:"bytes,9,rep,name=account_types,json=accountTypes,proto3" json:"account_types,omitempty"` + ResourceTypes []string `protobuf:"bytes,10,rep,name=resource_types,json=resourceTypes,proto3" json:"resource_types,omitempty"` } func (x *ListUserApprovalsRequest) Reset() { @@ -3013,12 +3016,34 @@ func (x *ListUserApprovalsRequest) GetAppealStatuses() []string { return nil } +func (x *ListUserApprovalsRequest) GetQ() string { + if x != nil { + return x.Q + } + return "" +} + +func (x *ListUserApprovalsRequest) GetAccountTypes() []string { + if x != nil { + return x.AccountTypes + } + return nil +} + +func (x *ListUserApprovalsRequest) GetResourceTypes() []string { + if x != nil { + return x.ResourceTypes + } + return nil +} + type ListUserApprovalsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Approvals []*Approval `protobuf:"bytes,1,rep,name=approvals,proto3" json:"approvals,omitempty"` + Total int32 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` } func (x *ListUserApprovalsResponse) Reset() { @@ -3060,6 +3085,13 @@ func (x *ListUserApprovalsResponse) GetApprovals() []*Approval { return nil } +func (x *ListUserApprovalsResponse) GetTotal() int32 { + if x != nil { + return x.Total + } + return 0 +} + type ListApprovalsRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -3072,6 +3104,9 @@ type ListApprovalsRequest struct { Size uint32 `protobuf:"varint,5,opt,name=size,proto3" json:"size,omitempty"` Offset uint32 `protobuf:"varint,6,opt,name=offset,proto3" json:"offset,omitempty"` AppealStatuses []string `protobuf:"bytes,7,rep,name=appeal_statuses,json=appealStatuses,proto3" json:"appeal_statuses,omitempty"` + Q string `protobuf:"bytes,8,opt,name=q,proto3" json:"q,omitempty"` + AccountTypes []string `protobuf:"bytes,9,rep,name=account_types,json=accountTypes,proto3" json:"account_types,omitempty"` + ResourceTypes []string `protobuf:"bytes,10,rep,name=resource_types,json=resourceTypes,proto3" json:"resource_types,omitempty"` } func (x *ListApprovalsRequest) Reset() { @@ -3155,12 +3190,34 @@ func (x *ListApprovalsRequest) GetAppealStatuses() []string { return nil } +func (x *ListApprovalsRequest) GetQ() string { + if x != nil { + return x.Q + } + return "" +} + +func (x *ListApprovalsRequest) GetAccountTypes() []string { + if x != nil { + return x.AccountTypes + } + return nil +} + +func (x *ListApprovalsRequest) GetResourceTypes() []string { + if x != nil { + return x.ResourceTypes + } + return nil +} + type ListApprovalsResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Approvals []*Approval `protobuf:"bytes,1,rep,name=approvals,proto3" json:"approvals,omitempty"` + Total int32 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` } func (x *ListApprovalsResponse) Reset() { @@ -3202,6 +3259,13 @@ func (x *ListApprovalsResponse) GetApprovals() []*Approval { return nil } +func (x *ListApprovalsResponse) GetTotal() int32 { + if x != nil { + return x.Total + } + return 0 +} + type UpdateApprovalRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -7227,7 +7291,7 @@ var file_gotocompany_guardian_v1beta1_guardian_proto_rawDesc = []byte{ 0x0b, 0x32, 0x24, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x65, 0x61, 0x6c, 0x52, 0x07, 0x61, 0x70, 0x70, 0x65, 0x61, 0x6c, 0x73, - 0x22, 0xdb, 0x01, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x41, 0x70, 0x70, + 0x22, 0xb5, 0x02, 0x0a, 0x18, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x6f, 0x72, 0x64, @@ -7240,14 +7304,21 @@ var file_gotocompany_guardian_v1beta1_guardian_proto_rawDesc = []byte{ 0x28, 0x0d, 0x42, 0x09, 0xfa, 0x42, 0x06, 0x2a, 0x04, 0x28, 0x00, 0x40, 0x01, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x70, 0x70, 0x65, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, - 0x61, 0x70, 0x70, 0x65, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x22, 0x61, - 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, - 0x61, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x61, - 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, - 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x67, 0x75, 0x61, - 0x72, 0x64, 0x69, 0x61, 0x6e, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x41, 0x70, - 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x52, 0x09, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, - 0x73, 0x22, 0xf6, 0x01, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, + 0x61, 0x70, 0x70, 0x65, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x12, 0x0c, + 0x0a, 0x01, 0x71, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x01, 0x71, 0x12, 0x23, 0x0a, 0x0d, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x09, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, + 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x73, 0x22, 0x77, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, + 0x55, 0x73, 0x65, 0x72, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x73, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, + 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, + 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x2e, + 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, + 0x52, 0x09, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x22, 0xd0, 0x02, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x74, 0x61, @@ -7262,13 +7333,20 @@ var file_gotocompany_guardian_v1beta1_guardian_proto_rawDesc = []byte{ 0xfa, 0x42, 0x06, 0x2a, 0x04, 0x28, 0x00, 0x40, 0x01, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x27, 0x0a, 0x0f, 0x61, 0x70, 0x70, 0x65, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x70, 0x70, 0x65, - 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x22, 0x5d, 0x0a, 0x15, 0x4c, 0x69, - 0x73, 0x74, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, 0x09, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x73, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, - 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x67, 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x2e, 0x76, 0x31, - 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x52, 0x09, - 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x73, 0x22, 0xda, 0x01, 0x0a, 0x15, 0x55, 0x70, + 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x12, 0x0c, 0x0a, 0x01, 0x71, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x01, 0x71, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x54, 0x79, 0x70, 0x65, 0x73, 0x12, 0x25, 0x0a, + 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x18, + 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, + 0x79, 0x70, 0x65, 0x73, 0x22, 0x73, 0x0a, 0x15, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x70, 0x70, 0x72, + 0x6f, 0x76, 0x61, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, + 0x09, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x26, 0x2e, 0x67, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6d, 0x70, 0x61, 0x6e, 0x79, 0x2e, 0x67, + 0x75, 0x61, 0x72, 0x64, 0x69, 0x61, 0x6e, 0x2e, 0x76, 0x31, 0x62, 0x65, 0x74, 0x61, 0x31, 0x2e, + 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x52, 0x09, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76, + 0x61, 0x6c, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x22, 0xda, 0x01, 0x0a, 0x15, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x70, 0x70, 0x72, 0x6f, 0x76, 0x61, 0x6c, 0x5f, diff --git a/core/approval/mocks/repository.go b/core/approval/mocks/repository.go index f0f5f3cee..1afff3bd8 100644 --- a/core/approval/mocks/repository.go +++ b/core/approval/mocks/repository.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.20.0. DO NOT EDIT. package mocks @@ -42,8 +42,8 @@ type Repository_AddApprover_Call struct { } // AddApprover is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 *domain.Approver +// - _a0 context.Context +// - _a1 *domain.Approver func (_e *Repository_Expecter) AddApprover(_a0 interface{}, _a1 interface{}) *Repository_AddApprover_Call { return &Repository_AddApprover_Call{Call: _e.mock.On("AddApprover", _a0, _a1)} } @@ -60,6 +60,11 @@ func (_c *Repository_AddApprover_Call) Return(_a0 error) *Repository_AddApprover return _c } +func (_c *Repository_AddApprover_Call) RunAndReturn(run func(context.Context, *domain.Approver) error) *Repository_AddApprover_Call { + _c.Call.Return(run) + return _c +} + // BulkInsert provides a mock function with given fields: _a0, _a1 func (_m *Repository) BulkInsert(_a0 context.Context, _a1 []*domain.Approval) error { ret := _m.Called(_a0, _a1) @@ -80,8 +85,8 @@ type Repository_BulkInsert_Call struct { } // BulkInsert is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 []*domain.Approval +// - _a0 context.Context +// - _a1 []*domain.Approval func (_e *Repository_Expecter) BulkInsert(_a0 interface{}, _a1 interface{}) *Repository_BulkInsert_Call { return &Repository_BulkInsert_Call{Call: _e.mock.On("BulkInsert", _a0, _a1)} } @@ -98,6 +103,11 @@ func (_c *Repository_BulkInsert_Call) Return(_a0 error) *Repository_BulkInsert_C return _c } +func (_c *Repository_BulkInsert_Call) RunAndReturn(run func(context.Context, []*domain.Approval) error) *Repository_BulkInsert_Call { + _c.Call.Return(run) + return _c +} + // DeleteApprover provides a mock function with given fields: ctx, approvalID, email func (_m *Repository) DeleteApprover(ctx context.Context, approvalID string, email string) error { ret := _m.Called(ctx, approvalID, email) @@ -118,9 +128,9 @@ type Repository_DeleteApprover_Call struct { } // DeleteApprover is a helper method to define mock.On call -// - ctx context.Context -// - approvalID string -// - email string +// - ctx context.Context +// - approvalID string +// - email string func (_e *Repository_Expecter) DeleteApprover(ctx interface{}, approvalID interface{}, email interface{}) *Repository_DeleteApprover_Call { return &Repository_DeleteApprover_Call{Call: _e.mock.On("DeleteApprover", ctx, approvalID, email)} } @@ -137,11 +147,73 @@ func (_c *Repository_DeleteApprover_Call) Return(_a0 error) *Repository_DeleteAp return _c } +func (_c *Repository_DeleteApprover_Call) RunAndReturn(run func(context.Context, string, string) error) *Repository_DeleteApprover_Call { + _c.Call.Return(run) + return _c +} + +// GetApprovalsTotalCount provides a mock function with given fields: _a0, _a1 +func (_m *Repository) GetApprovalsTotalCount(_a0 context.Context, _a1 *domain.ListApprovalsFilter) (int64, error) { + ret := _m.Called(_a0, _a1) + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *domain.ListApprovalsFilter) (int64, error)); ok { + return rf(_a0, _a1) + } + if rf, ok := ret.Get(0).(func(context.Context, *domain.ListApprovalsFilter) int64); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, *domain.ListApprovalsFilter) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Repository_GetApprovalsTotalCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetApprovalsTotalCount' +type Repository_GetApprovalsTotalCount_Call struct { + *mock.Call +} + +// GetApprovalsTotalCount is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 *domain.ListApprovalsFilter +func (_e *Repository_Expecter) GetApprovalsTotalCount(_a0 interface{}, _a1 interface{}) *Repository_GetApprovalsTotalCount_Call { + return &Repository_GetApprovalsTotalCount_Call{Call: _e.mock.On("GetApprovalsTotalCount", _a0, _a1)} +} + +func (_c *Repository_GetApprovalsTotalCount_Call) Run(run func(_a0 context.Context, _a1 *domain.ListApprovalsFilter)) *Repository_GetApprovalsTotalCount_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*domain.ListApprovalsFilter)) + }) + return _c +} + +func (_c *Repository_GetApprovalsTotalCount_Call) Return(_a0 int64, _a1 error) *Repository_GetApprovalsTotalCount_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Repository_GetApprovalsTotalCount_Call) RunAndReturn(run func(context.Context, *domain.ListApprovalsFilter) (int64, error)) *Repository_GetApprovalsTotalCount_Call { + _c.Call.Return(run) + return _c +} + // ListApprovals provides a mock function with given fields: _a0, _a1 func (_m *Repository) ListApprovals(_a0 context.Context, _a1 *domain.ListApprovalsFilter) ([]*domain.Approval, error) { ret := _m.Called(_a0, _a1) var r0 []*domain.Approval + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *domain.ListApprovalsFilter) ([]*domain.Approval, error)); ok { + return rf(_a0, _a1) + } if rf, ok := ret.Get(0).(func(context.Context, *domain.ListApprovalsFilter) []*domain.Approval); ok { r0 = rf(_a0, _a1) } else { @@ -150,7 +222,6 @@ func (_m *Repository) ListApprovals(_a0 context.Context, _a1 *domain.ListApprova } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, *domain.ListApprovalsFilter) error); ok { r1 = rf(_a0, _a1) } else { @@ -166,8 +237,8 @@ type Repository_ListApprovals_Call struct { } // ListApprovals is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 *domain.ListApprovalsFilter +// - _a0 context.Context +// - _a1 *domain.ListApprovalsFilter func (_e *Repository_Expecter) ListApprovals(_a0 interface{}, _a1 interface{}) *Repository_ListApprovals_Call { return &Repository_ListApprovals_Call{Call: _e.mock.On("ListApprovals", _a0, _a1)} } @@ -184,6 +255,11 @@ func (_c *Repository_ListApprovals_Call) Return(_a0 []*domain.Approval, _a1 erro return _c } +func (_c *Repository_ListApprovals_Call) RunAndReturn(run func(context.Context, *domain.ListApprovalsFilter) ([]*domain.Approval, error)) *Repository_ListApprovals_Call { + _c.Call.Return(run) + return _c +} + type mockConstructorTestingTNewRepository interface { mock.TestingT Cleanup(func()) diff --git a/core/approval/service.go b/core/approval/service.go index 105ac151d..0eb1abc74 100644 --- a/core/approval/service.go +++ b/core/approval/service.go @@ -9,6 +9,7 @@ import ( //go:generate mockery --name=repository --exported --with-expecter type repository interface { BulkInsert(context.Context, []*domain.Approval) error + GetApprovalsTotalCount(context.Context, *domain.ListApprovalsFilter) (int64, error) ListApprovals(context.Context, *domain.ListApprovalsFilter) ([]*domain.Approval, error) AddApprover(context.Context, *domain.Approver) error DeleteApprover(ctx context.Context, approvalID, email string) error @@ -39,6 +40,10 @@ func (s *Service) ListApprovals(ctx context.Context, filters *domain.ListApprova return s.repo.ListApprovals(ctx, filters) } +func (s *Service) GetApprovalsTotalCount(ctx context.Context, filters *domain.ListApprovalsFilter) (int64, error) { + return s.repo.GetApprovalsTotalCount(ctx, filters) +} + func (s *Service) BulkInsert(ctx context.Context, approvals []*domain.Approval) error { return s.repo.BulkInsert(ctx, approvals) } diff --git a/core/approval/service_test.go b/core/approval/service_test.go index f0c8fdfca..23a46799a 100644 --- a/core/approval/service_test.go +++ b/core/approval/service_test.go @@ -35,6 +35,62 @@ func (s *ServiceTestSuite) SetupTest() { }) } +func (s *ServiceTestSuite) TestListApprovals() { + s.Run("should return error if got error from repository", func() { + expectedError := errors.New("repository error") + s.mockRepository.EXPECT(). + ListApprovals(mock.AnythingOfType("*context.emptyCtx"), mock.Anything). + Return(nil, expectedError).Once() + + actualApprovals, actualError := s.service.ListApprovals(context.Background(), &domain.ListApprovalsFilter{}) + + s.Nil(actualApprovals) + s.EqualError(actualError, expectedError.Error()) + }) + + s.Run("should return approvals from repository", func() { + expectedApprovals := []*domain.Approval{ + { + ID: uuid.New().String(), + }, + } + s.mockRepository.EXPECT(). + ListApprovals(mock.AnythingOfType("*context.emptyCtx"), mock.Anything). + Return(expectedApprovals, nil).Once() + + actualApprovals, actualError := s.service.ListApprovals(context.Background(), &domain.ListApprovalsFilter{}) + + s.Equal(expectedApprovals, actualApprovals) + s.NoError(actualError) + }) +} + +func (s *ServiceTestSuite) TestGetApprovalsTotalCount() { + s.Run("should return error if got error from repository", func() { + expectedError := errors.New("repository error") + s.mockRepository.EXPECT(). + GetApprovalsTotalCount(mock.AnythingOfType("*context.emptyCtx"), mock.Anything). + Return(0, expectedError).Once() + + actualCount, actualError := s.service.GetApprovalsTotalCount(context.Background(), &domain.ListApprovalsFilter{}) + + s.Zero(actualCount) + s.EqualError(actualError, expectedError.Error()) + }) + + s.Run("should return approvals count from repository", func() { + expectedCount := int64(1) + s.mockRepository.EXPECT(). + GetApprovalsTotalCount(mock.AnythingOfType("*context.emptyCtx"), mock.Anything). + Return(expectedCount, nil).Once() + + actualCount, actualError := s.service.GetApprovalsTotalCount(context.Background(), &domain.ListApprovalsFilter{}) + + s.Equal(expectedCount, actualCount) + s.NoError(actualError) + }) +} + func (s *ServiceTestSuite) TestBulkInsert() { s.Run("should return error if got error from repository", func() { expectedError := errors.New("repository error") diff --git a/domain/approval.go b/domain/approval.go index 75275b827..88dd793d1 100644 --- a/domain/approval.go +++ b/domain/approval.go @@ -47,7 +47,10 @@ func (a *Approval) IsManualApproval() bool { } type ListApprovalsFilter struct { + Q string `mapstructure:"q" validate:"omitempty"` AccountID string `mapstructure:"account_id" validate:"omitempty,required"` + AccountTypes []string `mapstructure:"account_types" validate:"omitempty,min=1"` + ResourceTypes []string `mapstructure:"resource_types" validate:"omitempty,min=1"` CreatedBy string `mapstructure:"created_by" validate:"omitempty,required"` Statuses []string `mapstructure:"statuses" validate:"omitempty,min=1"` OrderBy []string `mapstructure:"order_by" validate:"omitempty,min=1"` diff --git a/go.mod b/go.mod index efd274d92..72580d360 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.11.2 go.opentelemetry.io/otel/sdk v1.11.2 golang.org/x/net v0.6.0 + golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 golang.org/x/sync v0.1.0 google.golang.org/api v0.106.0 google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f @@ -47,7 +48,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 gorm.io/datatypes v1.0.0 gorm.io/driver/postgres v1.4.7 - gorm.io/gorm v1.24.3 + gorm.io/gorm v1.25.1 ) require ( @@ -145,7 +146,6 @@ require ( go.uber.org/multierr v1.7.0 // indirect go.uber.org/zap v1.19.0 // indirect golang.org/x/crypto v0.6.0 // indirect - golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect golang.org/x/sys v0.5.0 // indirect golang.org/x/term v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect diff --git a/go.sum b/go.sum index 62d29d81d..a717718bd 100644 --- a/go.sum +++ b/go.sum @@ -2146,8 +2146,8 @@ gorm.io/gorm v1.20.5/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.24.2/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= -gorm.io/gorm v1.24.3 h1:WL2ifUmzR/SLp85CSURAfybcHnGZ+yLSGSxgYXlFBHg= -gorm.io/gorm v1.24.3/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= +gorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64= +gorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= diff --git a/internal/store/postgres/approval_repository.go b/internal/store/postgres/approval_repository.go index b26fc102e..0c17d3271 100644 --- a/internal/store/postgres/approval_repository.go +++ b/internal/store/postgres/approval_repository.go @@ -29,47 +29,21 @@ func NewApprovalRepository(db *gorm.DB) *ApprovalRepository { return &ApprovalRepository{db} } -func (r *ApprovalRepository) ListApprovals(ctx context.Context, conditions *domain.ListApprovalsFilter) ([]*domain.Approval, error) { - if err := utils.ValidateStruct(conditions); err != nil { +func (r *ApprovalRepository) ListApprovals(ctx context.Context, filter *domain.ListApprovalsFilter) ([]*domain.Approval, error) { + if err := utils.ValidateStruct(filter); err != nil { return nil, err } records := []*domain.Approval{} db := r.db.WithContext(ctx) - db = db.Preload("Appeal.Resource") - db = db.Joins("Appeal") - db = db.Joins(`JOIN "approvers" ON "approvals"."id" = "approvers"."approval_id"`) - - if conditions.CreatedBy != "" { - db = db.Where(`"approvers"."email" = ?`, conditions.CreatedBy) - } - if conditions.Statuses != nil { - db = db.Where(`"approvals"."status" IN ?`, conditions.Statuses) - } - if conditions.AccountID != "" { - db = db.Where(`"Appeal"."account_id" = ?`, conditions.AccountID) - } - - if len(conditions.AppealStatuses) == 0 { - db = db.Where(`"Appeal"."status" != ?`, domain.AppealStatusCanceled) - } else { - db = db.Where(`"Appeal"."status" IN ?`, conditions.AppealStatuses) - } - - if conditions.OrderBy != nil { - db = addOrderByClause(db, conditions.OrderBy, addOrderByClauseOptions{ - statusColumnName: `"approvals"."status"`, - statusesOrder: AppealStatusDefaultSort, - }) + db = applyFilter(db, filter) + if filter.Size > 0 { + db = db.Limit(filter.Size) } - if conditions.Size > 0 { - db = db.Limit(conditions.Size) - } - - if conditions.Offset > 0 { - db = db.Offset(conditions.Offset) + if filter.Offset > 0 { + db = db.Offset(filter.Offset) } var models []*model.Approval @@ -89,6 +63,18 @@ func (r *ApprovalRepository) ListApprovals(ctx context.Context, conditions *doma return records, nil } +func (r *ApprovalRepository) GetApprovalsTotalCount(ctx context.Context, filter *domain.ListApprovalsFilter) (int64, error) { + db := r.db.WithContext(ctx) + db = applyFilter(db, filter) + + var count int64 + if err := db.Model(&model.Approval{}).Count(&count).Error; err != nil { + return 0, err + } + + return count, nil +} + func (r *ApprovalRepository) BulkInsert(ctx context.Context, approvals []*domain.Approval) error { models := []*model.Approval{} for _, a := range approvals { @@ -149,3 +135,49 @@ func (r *ApprovalRepository) DeleteApprover(ctx context.Context, approvalID, ema return nil } + +func applyFilter(db *gorm.DB, filter *domain.ListApprovalsFilter) *gorm.DB { + db = db.Joins("Appeal"). + Joins("Appeal.Resource"). + Joins(`JOIN "approvers" ON "approvals"."id" = "approvers"."approval_id"`) + + if filter.Q != "" { + // NOTE: avoid adding conditions before this grouped where clause. + // Otherwise, it will be wrapped in parentheses and the query will be invalid. + db = db.Where(db. + Where(`"Appeal"."account_id" LIKE ?`, fmt.Sprintf("%%%s%%", filter.Q)). + Or(`"Appeal"."role" LIKE ?`, fmt.Sprintf("%%%s%%", filter.Q)). + Or(`"Appeal__Resource"."urn" LIKE ?`, fmt.Sprintf("%%%s%%", filter.Q)), + ) + } + if filter.CreatedBy != "" { + db = db.Where(`"approvers"."email" = ?`, filter.CreatedBy) + } + if filter.Statuses != nil { + db = db.Where(`"approvals"."status" IN ?`, filter.Statuses) + } + if filter.AccountID != "" { + db = db.Where(`"Appeal"."account_id" = ?`, filter.AccountID) + } + if filter.AccountTypes != nil { + db = db.Where(`"Appeal"."account_type" IN ?`, filter.AccountTypes) + } + if filter.ResourceTypes != nil { + db = db.Where(`"Appeal__Resource"."type" IN ?`, filter.ResourceTypes) + } + + if len(filter.AppealStatuses) == 0 { + db = db.Where(`"Appeal"."status" != ?`, domain.AppealStatusCanceled) + } else { + db = db.Where(`"Appeal"."status" IN ?`, filter.AppealStatuses) + } + + if filter.OrderBy != nil { + db = addOrderByClause(db, filter.OrderBy, addOrderByClauseOptions{ + statusColumnName: `"approvals"."status"`, + statusesOrder: AppealStatusDefaultSort, + }) + } + + return db +} diff --git a/internal/store/postgres/approval_repository_test.go b/internal/store/postgres/approval_repository_test.go index 7154a49e3..045fc6920 100644 --- a/internal/store/postgres/approval_repository_test.go +++ b/internal/store/postgres/approval_repository_test.go @@ -119,7 +119,7 @@ func (s *ApprovalRepositoryTestSuite) TearDownSuite() { func (s *ApprovalRepositoryTestSuite) TestListApprovals() { - appealA := &domain.Appeal{ + pendingAppeal := &domain.Appeal{ ResourceID: s.dummyResource.ID, PolicyID: s.dummyPolicy.ID, PolicyVersion: s.dummyPolicy.Version, @@ -131,7 +131,7 @@ func (s *ApprovalRepositoryTestSuite) TestListApprovals() { Status: domain.AppealStatusPending, } - appealB := &domain.Appeal{ + cancelledAppeal := &domain.Appeal{ ResourceID: s.dummyResource.ID, PolicyID: s.dummyPolicy.ID, PolicyVersion: s.dummyPolicy.Version, @@ -143,7 +143,7 @@ func (s *ApprovalRepositoryTestSuite) TestListApprovals() { Status: domain.AppealStatusCanceled, } - s.appealRepository.BulkUpsert(context.Background(), []*domain.Appeal{appealA, appealB}) + s.appealRepository.BulkUpsert(context.Background(), []*domain.Appeal{pendingAppeal, cancelledAppeal}) dummyApprovals := []*domain.Approval{ { @@ -176,20 +176,20 @@ func (s *ApprovalRepositoryTestSuite) TestListApprovals() { { Name: "test-approval-name-4", Index: 1, - AppealID: appealA.ID, + AppealID: pendingAppeal.ID, Status: "test-status-1", PolicyID: "test-policy-id", PolicyVersion: 1, - Appeal: appealA, + Appeal: pendingAppeal, }, { Name: "test-approval-name-5", Index: 1, - AppealID: appealB.ID, + AppealID: cancelledAppeal.ID, Status: "test-status-1", PolicyID: "test-policy-id", PolicyVersion: 1, - Appeal: appealB, + Appeal: cancelledAppeal, }, } @@ -214,12 +214,12 @@ func (s *ApprovalRepositoryTestSuite) TestListApprovals() { }, { ApprovalID: dummyApprovals[3].ID, - AppealID: appealA.ID, + AppealID: pendingAppeal.ID, Email: "approver3@email.com", }, { ApprovalID: dummyApprovals[4].ID, - AppealID: appealB.ID, + AppealID: cancelledAppeal.ID, Email: "approver3@email.com", }, } @@ -245,9 +245,37 @@ func (s *ApprovalRepositoryTestSuite) TestListApprovals() { s.Equal(dummyApprovals[0].ID, approvals[0].ID) }) + s.Run("should return approvals based on query search input", func() { + approvals, err := s.repository.ListApprovals(context.Background(), &domain.ListApprovalsFilter{ + Q: "abc-user", // expected to match account_id: "abc-user@example.com" + }) + + s.NoError(err) + s.Len(approvals, 1) + s.Equal(pendingAppeal.ID, approvals[0].AppealID) + }) + + s.Run("should return list of approvals based account types filter", func() { + approvals, err := s.repository.ListApprovals(context.Background(), &domain.ListApprovalsFilter{ + AccountTypes: []string{"x-account-type"}, // match 0 records + }) + + s.NoError(err) + s.Len(approvals, 0) + }) + + s.Run("should return list of approvals based resource types filter", func() { + approvals, err := s.repository.ListApprovals(context.Background(), &domain.ListApprovalsFilter{ + ResourceTypes: []string{"x-resource-type"}, // match 0 records + }) + + s.NoError(err) + s.Len(approvals, 0) + }) + s.Run("should return list of approvals where appeal status is canceled", func() { approvals, err := s.repository.ListApprovals(context.Background(), &domain.ListApprovalsFilter{ - AccountID: appealB.AccountID, + AccountID: cancelledAppeal.AccountID, CreatedBy: dummyApprover[3].Email, AppealStatuses: []string{domain.AppealStatusCanceled}, OrderBy: []string{"status", "updated_at:desc", "created_at"}, @@ -260,7 +288,7 @@ func (s *ApprovalRepositoryTestSuite) TestListApprovals() { s.Run("should return list of approvals where appeal status is pending", func() { approvals, err := s.repository.ListApprovals(context.Background(), &domain.ListApprovalsFilter{ - AccountID: appealA.AccountID, + AccountID: pendingAppeal.AccountID, CreatedBy: dummyApprover[3].Email, AppealStatuses: []string{domain.AppealStatusPending}, OrderBy: []string{"status", "updated_at:desc", "created_at"}, @@ -293,6 +321,86 @@ func (s *ApprovalRepositoryTestSuite) TestListApprovals() { }) } +func (s *ApprovalRepositoryTestSuite) TestListApprovals__Search() { + s.Run("should pass grouped condition properly and return result accordingly", func() { + dummyAppeals := []*domain.Appeal{ + { + ResourceID: s.dummyResource.ID, + PolicyID: s.dummyPolicy.ID, + PolicyVersion: s.dummyPolicy.Version, + AccountID: "user1@example.com", + AccountType: domain.DefaultAppealAccountType, + Role: "role_test", + Permissions: []string{"permission_test"}, + CreatedBy: "user1@example.com", + Status: domain.AppealStatusPending, + }, + { + ResourceID: s.dummyResource.ID, + PolicyID: s.dummyPolicy.ID, + PolicyVersion: s.dummyPolicy.Version, + AccountID: "user2@example.com", + AccountType: domain.DefaultAppealAccountType, + Role: "role_test", + Permissions: []string{"permission_test"}, + CreatedBy: "user2@example.com", + Status: domain.AppealStatusPending, + }, + } + err := s.appealRepository.BulkUpsert(context.Background(), dummyAppeals) + s.Require().NoError(err) + + dummyApprovals := []*domain.Approval{ + { + Name: "test-approval-name-1", + Index: 0, + AppealID: dummyAppeals[0].ID, + Status: domain.ApprovalStatusPending, + PolicyID: "test-policy-id", + PolicyVersion: 1, + Appeal: dummyAppeals[0], + }, + { + Name: "test-approval-name-1", + Index: 0, + AppealID: dummyAppeals[1].ID, + Status: domain.ApprovalStatusPending, + PolicyID: "test-policy-id", + PolicyVersion: 1, + Appeal: dummyAppeals[1], + }, + } + err = s.repository.BulkInsert(context.Background(), dummyApprovals) + s.Require().NoError(err) + + dummyApprover := []*domain.Approver{ + { + ApprovalID: dummyApprovals[0].ID, + AppealID: dummyAppeals[0].ID, + Email: "approver@email.com", + }, + { + ApprovalID: dummyApprovals[1].ID, + AppealID: dummyAppeals[1].ID, + Email: "approver2@email.com", + }, + } + for _, ap := range dummyApprover { + err = s.repository.AddApprover(context.Background(), ap) + s.Require().NoError(err) + } + + approvals, err := s.repository.ListApprovals(context.Background(), &domain.ListApprovalsFilter{ + CreatedBy: "approver@email.com", + Q: "role_test", + }) + + s.NoError(err) + s.Len(approvals, 1) + s.Equal(dummyApprovals[0].ID, approvals[0].ID) + }) +} + func (s *ApprovalRepositoryTestSuite) TestBulkInsert() { actor := "user@email.com" approvals := []*domain.Approval{ diff --git a/internal/store/postgres/migrations/000016_add-indexes-for-approvals-filter.down.sql b/internal/store/postgres/migrations/000016_add-indexes-for-approvals-filter.down.sql new file mode 100644 index 000000000..157863585 --- /dev/null +++ b/internal/store/postgres/migrations/000016_add-indexes-for-approvals-filter.down.sql @@ -0,0 +1,5 @@ +DROP INDEX IF EXISTS idx_appeals_account_id_at_null; +DROP INDEX IF EXISTS idx_appeals_account_type_at_null; +DROP INDEX IF EXISTS idx_appeals_role_at_null; +DROP INDEX IF EXISTS idx_resources_type_at_null; +DROP INDEX IF EXISTS idx_resources_urn_at_null; \ No newline at end of file diff --git a/internal/store/postgres/migrations/000016_add-indexes-for-approvals-filter.up.sql b/internal/store/postgres/migrations/000016_add-indexes-for-approvals-filter.up.sql new file mode 100644 index 000000000..5a1d73e24 --- /dev/null +++ b/internal/store/postgres/migrations/000016_add-indexes-for-approvals-filter.up.sql @@ -0,0 +1,19 @@ +CREATE INDEX if NOT EXISTS idx_appeals_account_id_at_null ON appeals (account_id, deleted_at) +WHERE + deleted_at IS NULL; + +CREATE INDEX if NOT EXISTS idx_appeals_account_type_at_null ON appeals (account_type, deleted_at) +WHERE + deleted_at IS NULL; + +CREATE INDEX if NOT EXISTS idx_appeals_role_at_null ON appeals (role, deleted_at) +WHERE + deleted_at IS NULL; + +CREATE INDEX if NOT EXISTS idx_resources_type_at_null ON resources (type, deleted_at) +WHERE + deleted_at IS NULL; + +CREATE INDEX if NOT EXISTS idx_resources_urn_at_null ON resources (urn, deleted_at) +WHERE + deleted_at IS NULL; \ No newline at end of file diff --git a/third_party/OpenAPI/gotocompany/guardian/v1beta1/guardian.swagger.json b/third_party/OpenAPI/gotocompany/guardian/v1beta1/guardian.swagger.json index 80d3f877e..6dbbb3f80 100644 --- a/third_party/OpenAPI/gotocompany/guardian/v1beta1/guardian.swagger.json +++ b/third_party/OpenAPI/gotocompany/guardian/v1beta1/guardian.swagger.json @@ -559,6 +559,32 @@ "type": "string" }, "collectionFormat": "multi" + }, + { + "name": "q", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "accountTypes", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "resourceTypes", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" } ], "tags": [ @@ -1037,6 +1063,32 @@ "type": "string" }, "collectionFormat": "multi" + }, + { + "name": "q", + "in": "query", + "required": false, + "type": "string" + }, + { + "name": "accountTypes", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + }, + { + "name": "resourceTypes", + "in": "query", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" } ], "tags": [ @@ -2628,6 +2680,10 @@ "type": "object", "$ref": "#/definitions/v1beta1Approval" } + }, + "total": { + "type": "integer", + "format": "int32" } } }, @@ -2712,6 +2768,10 @@ "type": "object", "$ref": "#/definitions/v1beta1Approval" } + }, + "total": { + "type": "integer", + "format": "int32" } } },