forked from raystack/guardian
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(comment): introduce appeal comments (#127)
* feat(comment): introduce appeal comments * fix: check appeal existance on list comments * test: add repository and service tests * test: add test for comment handler * chore: update proton * chore: update ctx with cancel * chore: update dependancy to appeal service * chore: use appeal service struct and update tests * refactor: use centralized interface and mocks * refactor: put default order by in a constant * feat: introduce parent type * chore: update proton commit * chore: update proton commit * chore: update proton commit * chore: update proton commit --------- Co-authored-by: Muhammad Idil Haq Amir <[email protected]>
- Loading branch information
Showing
31 changed files
with
3,899 additions
and
1,543 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
package v1beta1 | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
|
||
guardianv1beta1 "github.com/goto/guardian/api/proto/gotocompany/guardian/v1beta1" | ||
"github.com/goto/guardian/core/appeal" | ||
"github.com/goto/guardian/core/comment" | ||
"github.com/goto/guardian/domain" | ||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/status" | ||
) | ||
|
||
func (s *GRPCServer) ListAppealComments(ctx context.Context, req *guardianv1beta1.ListAppealCommentsRequest) (*guardianv1beta1.ListAppealCommentsResponse, error) { | ||
comments, err := s.appealService.ListComments(ctx, domain.ListCommentsFilter{ | ||
ParentID: req.GetAppealId(), | ||
OrderBy: req.GetOrderBy(), | ||
}) | ||
if err != nil { | ||
switch { | ||
case errors.Is(err, appeal.ErrAppealNotFound): | ||
return nil, status.Errorf(codes.NotFound, err.Error()) | ||
default: | ||
return nil, s.internalError(ctx, "failed to list comments: %s", err) | ||
} | ||
} | ||
|
||
commentProtos := []*guardianv1beta1.AppealComment{} | ||
for _, c := range comments { | ||
commentProtos = append(commentProtos, s.adapter.ToCommentProto(c)) | ||
} | ||
|
||
return &guardianv1beta1.ListAppealCommentsResponse{ | ||
Comments: commentProtos, | ||
}, nil | ||
} | ||
|
||
func (s *GRPCServer) CreateAppealComment(ctx context.Context, req *guardianv1beta1.CreateAppealCommentRequest) (*guardianv1beta1.CreateAppealCommentResponse, error) { | ||
actor, err := s.getUser(ctx) | ||
if err != nil { | ||
return nil, status.Error(codes.Unauthenticated, err.Error()) | ||
} | ||
|
||
c := &domain.Comment{ | ||
ParentID: req.GetAppealId(), | ||
Body: req.GetBody(), | ||
CreatedBy: actor, | ||
} | ||
if err := s.appealService.CreateComment(ctx, c); err != nil { | ||
switch { | ||
case | ||
errors.Is(err, comment.ErrEmptyCommentParentType), | ||
errors.Is(err, comment.ErrEmptyCommentParentID), | ||
errors.Is(err, comment.ErrEmptyCommentCreator), | ||
errors.Is(err, comment.ErrEmptyCommentBody): | ||
return nil, s.invalidArgument(ctx, err.Error()) | ||
case errors.Is(err, appeal.ErrAppealNotFound): | ||
return nil, status.Errorf(codes.NotFound, err.Error()) | ||
default: | ||
return nil, s.internalError(ctx, "failed to create comment: %s", err) | ||
} | ||
} | ||
|
||
return &guardianv1beta1.CreateAppealCommentResponse{ | ||
Comment: s.adapter.ToCommentProto(c), | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
package v1beta1_test | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"time" | ||
|
||
"github.com/google/uuid" | ||
guardianv1beta1 "github.com/goto/guardian/api/proto/gotocompany/guardian/v1beta1" | ||
"github.com/goto/guardian/core/appeal" | ||
"github.com/goto/guardian/core/comment" | ||
"github.com/goto/guardian/domain" | ||
"github.com/stretchr/testify/mock" | ||
"google.golang.org/grpc/codes" | ||
"google.golang.org/grpc/status" | ||
"google.golang.org/protobuf/types/known/timestamppb" | ||
) | ||
|
||
func (s *GrpcHandlersSuite) TestListComments() { | ||
s.Run("should return list of comments on success", func() { | ||
s.setup() | ||
timeNow := time.Now() | ||
|
||
appealID := uuid.New().String() | ||
dummyComments := []*domain.Comment{ | ||
{ | ||
ID: uuid.New().String(), | ||
ParentID: appealID, | ||
CreatedBy: "[email protected]", | ||
Body: "comment 1", | ||
CreatedAt: timeNow, | ||
UpdatedAt: timeNow, | ||
}, | ||
{ | ||
ID: uuid.New().String(), | ||
ParentID: appealID, | ||
CreatedBy: "[email protected]", | ||
Body: "comment 2", | ||
CreatedAt: timeNow, | ||
UpdatedAt: timeNow, | ||
}, | ||
} | ||
expectedResponse := &guardianv1beta1.ListAppealCommentsResponse{ | ||
Comments: []*guardianv1beta1.AppealComment{ | ||
{ | ||
Id: dummyComments[0].ID, | ||
AppealId: appealID, | ||
CreatedBy: "[email protected]", | ||
Body: "comment 1", | ||
CreatedAt: timestamppb.New(timeNow), | ||
UpdatedAt: timestamppb.New(timeNow), | ||
}, | ||
{ | ||
Id: dummyComments[1].ID, | ||
AppealId: appealID, | ||
CreatedBy: "[email protected]", | ||
Body: "comment 2", | ||
CreatedAt: timestamppb.New(timeNow), | ||
UpdatedAt: timestamppb.New(timeNow), | ||
}, | ||
}, | ||
} | ||
expectedOrderBy := []string{"created_at:desc"} | ||
|
||
s.appealService.EXPECT(). | ||
ListComments(mock.MatchedBy(func(ctx context.Context) bool { return true }), mock.AnythingOfType("domain.ListCommentsFilter")). | ||
Return(dummyComments, nil). | ||
Run(func(_a0 context.Context, filter domain.ListCommentsFilter) { | ||
s.Equal(appealID, filter.ParentID) | ||
s.Equal(expectedOrderBy, filter.OrderBy) | ||
}) | ||
defer s.appealService.AssertExpectations(s.T()) | ||
|
||
req := &guardianv1beta1.ListAppealCommentsRequest{ | ||
AppealId: appealID, | ||
OrderBy: expectedOrderBy, | ||
} | ||
res, err := s.grpcServer.ListAppealComments(context.Background(), req) | ||
|
||
s.NoError(err) | ||
s.Equal(expectedResponse, res) | ||
}) | ||
|
||
s.Run("should return error codes according to the service error", func() { | ||
testCases := []struct { | ||
name string | ||
expecedError error | ||
expectedGRPCCode codes.Code | ||
}{ | ||
{ | ||
name: "should return not found error when appeal not found", | ||
expecedError: appeal.ErrAppealNotFound, | ||
expectedGRPCCode: codes.NotFound, | ||
}, | ||
{ | ||
name: "should return internal error when service fails", | ||
expecedError: errors.New("unexpected error"), | ||
expectedGRPCCode: codes.Internal, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
s.Run(tc.name, func() { | ||
s.setup() | ||
|
||
appealID := uuid.New().String() | ||
s.appealService.EXPECT(). | ||
ListComments(mock.MatchedBy(func(ctx context.Context) bool { return true }), mock.AnythingOfType("domain.ListCommentsFilter")). | ||
Return(nil, tc.expecedError) | ||
defer s.appealService.AssertExpectations(s.T()) | ||
|
||
req := &guardianv1beta1.ListAppealCommentsRequest{ | ||
AppealId: appealID, | ||
} | ||
res, err := s.grpcServer.ListAppealComments(context.Background(), req) | ||
|
||
s.Equal(tc.expectedGRPCCode, status.Code(err)) | ||
s.Nil(res) | ||
}) | ||
} | ||
}) | ||
} | ||
|
||
func (s *GrpcHandlersSuite) TestCreateComment() { | ||
s.Run("should return comment on success", func() { | ||
s.setup() | ||
timeNow := time.Now() | ||
|
||
appealID := uuid.New().String() | ||
actor := "[email protected]" | ||
commentBody := "test comment" | ||
expectedNewComment := &domain.Comment{ | ||
ID: uuid.New().String(), | ||
ParentID: appealID, | ||
CreatedBy: actor, | ||
Body: commentBody, | ||
CreatedAt: timeNow, | ||
UpdatedAt: timeNow, | ||
} | ||
expectedResponse := &guardianv1beta1.CreateAppealCommentResponse{ | ||
Comment: &guardianv1beta1.AppealComment{ | ||
Id: expectedNewComment.ID, | ||
AppealId: appealID, | ||
CreatedBy: actor, | ||
Body: commentBody, | ||
CreatedAt: timestamppb.New(timeNow), | ||
UpdatedAt: timestamppb.New(timeNow), | ||
}, | ||
} | ||
|
||
s.appealService.EXPECT(). | ||
CreateComment(mock.MatchedBy(func(ctx context.Context) bool { return true }), mock.AnythingOfType("*domain.Comment")). | ||
Return(nil). | ||
Run(func(_a0 context.Context, c *domain.Comment) { | ||
s.Equal(appealID, c.ParentID) | ||
s.Equal(actor, c.CreatedBy) | ||
s.Equal(commentBody, c.Body) | ||
|
||
// updated values | ||
c.ID = expectedNewComment.ID | ||
c.CreatedAt = expectedNewComment.CreatedAt | ||
c.UpdatedAt = expectedNewComment.UpdatedAt | ||
}) | ||
defer s.appealService.AssertExpectations(s.T()) | ||
|
||
req := &guardianv1beta1.CreateAppealCommentRequest{ | ||
AppealId: appealID, | ||
Body: commentBody, | ||
} | ||
ctx := context.WithValue(context.Background(), authEmailTestContextKey{}, actor) | ||
res, err := s.grpcServer.CreateAppealComment(ctx, req) | ||
|
||
s.NoError(err) | ||
s.Equal(expectedResponse, res) | ||
}) | ||
|
||
s.Run("should return unauthenticated error when user is not authenticated", func() { | ||
s.setup() | ||
req := &guardianv1beta1.CreateAppealCommentRequest{ | ||
AppealId: uuid.New().String(), | ||
Body: "test comment content", | ||
} | ||
ctx := context.Background() // no authenticated user in context | ||
res, err := s.grpcServer.CreateAppealComment(ctx, req) | ||
|
||
s.Equal(codes.Unauthenticated, status.Code(err)) | ||
s.Nil(res) | ||
}) | ||
|
||
s.Run("should return error codes according to the service error", func() { | ||
testCases := []struct { | ||
name string | ||
expecedError error | ||
expectedGRPCCode codes.Code | ||
}{ | ||
{ | ||
name: "should return invalid argument error when parent type is empty", | ||
expecedError: comment.ErrEmptyCommentParentType, | ||
expectedGRPCCode: codes.InvalidArgument, | ||
}, | ||
{ | ||
name: "should return invalid argument error when parent id is empty", | ||
expecedError: comment.ErrEmptyCommentParentID, | ||
expectedGRPCCode: codes.InvalidArgument, | ||
}, | ||
{ | ||
name: "should return invalid argument error when comment creator is empty", | ||
expecedError: comment.ErrEmptyCommentCreator, | ||
expectedGRPCCode: codes.InvalidArgument, | ||
}, | ||
{ | ||
name: "should return invalid argument error when comment body is empty", | ||
expecedError: comment.ErrEmptyCommentBody, | ||
expectedGRPCCode: codes.InvalidArgument, | ||
}, | ||
{ | ||
name: "should return not found error when appeal not found", | ||
expecedError: appeal.ErrAppealNotFound, | ||
expectedGRPCCode: codes.NotFound, | ||
}, | ||
{ | ||
name: "should return internal error when service fails", | ||
expecedError: errors.New("unexpected error"), | ||
expectedGRPCCode: codes.Internal, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
s.Run(tc.name, func() { | ||
s.setup() | ||
|
||
s.appealService.EXPECT(). | ||
CreateComment(mock.MatchedBy(func(ctx context.Context) bool { return true }), mock.AnythingOfType("*domain.Comment")). | ||
Return(tc.expecedError) | ||
defer s.appealService.AssertExpectations(s.T()) | ||
|
||
req := &guardianv1beta1.CreateAppealCommentRequest{ | ||
AppealId: uuid.New().String(), | ||
Body: "test comment content", | ||
} | ||
ctx := context.WithValue(context.Background(), authEmailTestContextKey{}, "[email protected]") | ||
res, err := s.grpcServer.CreateAppealComment(ctx, req) | ||
|
||
s.Equal(tc.expectedGRPCCode, status.Code(err)) | ||
s.Nil(res) | ||
}) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.