diff --git a/internal/api/v1beta1connect/errors.go b/internal/api/v1beta1connect/errors.go index 74d7799fd..d1a6d58c3 100644 --- a/internal/api/v1beta1connect/errors.go +++ b/internal/api/v1beta1connect/errors.go @@ -50,4 +50,7 @@ var ( ErrNamespaceSplitNotation = errors.New("subject/object should be provided as 'namespace:uuid'") ErrPolicyNotFound = errors.New("policy doesn't exist") ErrProjectNotFound = errors.New("project doesn't exist") + ErrGroupNotFound = errors.New("group doesn't exist") + ErrOrgNotFound = errors.New("org doesn't exist") + ErrGroupMinOwnerCount = errors.New("group must have at least one owner, consider adding another owner before removing") ) diff --git a/internal/api/v1beta1connect/group.go b/internal/api/v1beta1connect/group.go index c9db98e99..cfe99b035 100644 --- a/internal/api/v1beta1connect/group.go +++ b/internal/api/v1beta1connect/group.go @@ -2,10 +2,21 @@ package v1beta1connect import ( "context" + "errors" "connectrpc.com/connect" + grpczap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "github.com/raystack/frontier/core/audit" "github.com/raystack/frontier/core/group" + "github.com/raystack/frontier/core/organization" + "github.com/raystack/frontier/core/role" + "github.com/raystack/frontier/core/user" + "github.com/raystack/frontier/internal/bootstrap/schema" + "github.com/raystack/frontier/pkg/metadata" + "github.com/raystack/frontier/pkg/str" + "github.com/raystack/frontier/pkg/utils" frontierv1beta1 "github.com/raystack/frontier/proto/v1beta1" + "go.uber.org/zap" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -32,6 +43,401 @@ func (h *ConnectHandler) ListGroups(ctx context.Context, request *connect.Reques return connect.NewResponse(&frontierv1beta1.ListGroupsResponse{Groups: groups}), nil } +func (h *ConnectHandler) ListOrganizationGroups(ctx context.Context, request *connect.Request[frontierv1beta1.ListOrganizationGroupsRequest]) (*connect.Response[frontierv1beta1.ListOrganizationGroupsResponse], error) { + orgResp, err := h.orgService.Get(ctx, request.Msg.GetOrgId()) + if err != nil { + switch { + case errors.Is(err, organization.ErrDisabled): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgDisabled) + case errors.Is(err, organization.ErrNotExist): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + + var groups []*frontierv1beta1.Group + groupList, err := h.groupService.List(ctx, group.Filter{ + OrganizationID: orgResp.ID, + State: group.State(request.Msg.GetState()), + GroupIDs: request.Msg.GetGroupIds(), + WithMemberCount: request.Msg.GetWithMemberCount(), + }) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + + for _, v := range groupList { + groupPB, err := transformGroupToPB(v) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + + if request.Msg.GetWithMembers() { + groupUsers, err := h.userService.ListByGroup(ctx, v.ID, "") + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + var groupUsersErr error + groupPB.Users = utils.Filter(utils.Map(groupUsers, func(user user.User) *frontierv1beta1.User { + pb, err := transformUserToPB(user) + if err != nil { + groupUsersErr = errors.Join(groupUsersErr, err) + return nil + } + return pb + }), func(user *frontierv1beta1.User) bool { + return user != nil + }) + if groupUsersErr != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + + groups = append(groups, &groupPB) + } + + return connect.NewResponse(&frontierv1beta1.ListOrganizationGroupsResponse{Groups: groups}), nil +} + +func (h *ConnectHandler) CreateGroup(ctx context.Context, request *connect.Request[frontierv1beta1.CreateGroupRequest]) (*connect.Response[frontierv1beta1.CreateGroupResponse], error) { + if request.Msg.GetBody() == nil { + return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) + } + + orgResp, err := h.orgService.Get(ctx, request.Msg.GetOrgId()) + if err != nil { + switch { + case errors.Is(err, organization.ErrDisabled): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgDisabled) + case errors.Is(err, organization.ErrNotExist): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + + metaDataMap := metadata.Build(request.Msg.GetBody().GetMetadata().AsMap()) + + if err := h.metaSchemaService.Validate(metaDataMap, groupMetaSchema); err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadBodyMetaSchemaError) + } + + // Auto-generate name from title if name is empty but title is provided + requestBody := request.Msg.GetBody() + name := requestBody.GetName() + if name == "" && requestBody.GetTitle() != "" { + name = str.GenerateSlug(requestBody.GetTitle()) + } + + newGroup, err := h.groupService.Create(ctx, group.Group{ + Name: name, + Title: requestBody.GetTitle(), + OrganizationID: orgResp.ID, + Metadata: metaDataMap, + }) + if err != nil { + switch { + case errors.Is(err, group.ErrConflict): + return nil, connect.NewError(connect.CodeAlreadyExists, ErrConflictRequest) + case errors.Is(err, group.ErrInvalidDetail), errors.Is(err, organization.ErrNotExist), errors.Is(err, organization.ErrInvalidUUID): + return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) + case errors.Is(err, user.ErrInvalidEmail): + return nil, connect.NewError(connect.CodeUnauthenticated, ErrUnauthenticated) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + + groupPB, err := transformGroupToPB(newGroup) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + + audit.GetAuditor(ctx, request.Msg.GetOrgId()).Log(audit.GroupCreatedEvent, audit.GroupTarget(newGroup.ID)) + return connect.NewResponse(&frontierv1beta1.CreateGroupResponse{Group: &groupPB}), nil +} + +func (h *ConnectHandler) GetGroup(ctx context.Context, request *connect.Request[frontierv1beta1.GetGroupRequest]) (*connect.Response[frontierv1beta1.GetGroupResponse], error) { + _, err := h.orgService.Get(ctx, request.Msg.GetOrgId()) + if err != nil { + switch { + case errors.Is(err, organization.ErrDisabled): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgDisabled) + case errors.Is(err, organization.ErrNotExist): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + + fetchedGroup, err := h.groupService.Get(ctx, request.Msg.GetId()) + if err != nil { + switch { + case errors.Is(err, group.ErrNotExist), errors.Is(err, group.ErrInvalidID), errors.Is(err, group.ErrInvalidUUID): + return nil, connect.NewError(connect.CodeNotFound, ErrGroupNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + + groupPB, err := transformGroupToPB(fetchedGroup) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + + if request.Msg.GetWithMembers() { + groupUsers, err := h.userService.ListByGroup(ctx, fetchedGroup.ID, "") + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + var groupUsersErr error + groupPB.Users = utils.Filter(utils.Map(groupUsers, func(user user.User) *frontierv1beta1.User { + pb, err := transformUserToPB(user) + if err != nil { + groupUsersErr = errors.Join(groupUsersErr, err) + return nil + } + return pb + }), func(user *frontierv1beta1.User) bool { + return user != nil + }) + if groupUsersErr != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + + return connect.NewResponse(&frontierv1beta1.GetGroupResponse{Group: &groupPB}), nil +} + +func (h *ConnectHandler) UpdateGroup(ctx context.Context, request *connect.Request[frontierv1beta1.UpdateGroupRequest]) (*connect.Response[frontierv1beta1.UpdateGroupResponse], error) { + if request.Msg.GetBody() == nil { + return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) + } + + orgResp, err := h.orgService.Get(ctx, request.Msg.GetOrgId()) + if err != nil { + switch { + case errors.Is(err, organization.ErrDisabled): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgDisabled) + case errors.Is(err, organization.ErrNotExist): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + + metaDataMap := metadata.Build(request.Msg.GetBody().GetMetadata().AsMap()) + + if err := h.metaSchemaService.Validate(metaDataMap, groupMetaSchema); err != nil { + return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadBodyMetaSchemaError) + } + + updatedGroup, err := h.groupService.Update(ctx, group.Group{ + ID: request.Msg.GetId(), + Name: request.Msg.GetBody().GetName(), + Title: request.Msg.GetBody().GetTitle(), + OrganizationID: orgResp.ID, + Metadata: metaDataMap, + }) + if err != nil { + switch { + case errors.Is(err, group.ErrNotExist), errors.Is(err, group.ErrInvalidUUID), errors.Is(err, group.ErrInvalidID): + return nil, connect.NewError(connect.CodeNotFound, ErrGroupNotFound) + case errors.Is(err, group.ErrConflict): + return nil, connect.NewError(connect.CodeAlreadyExists, ErrConflictRequest) + case errors.Is(err, group.ErrInvalidDetail), errors.Is(err, organization.ErrInvalidUUID), errors.Is(err, organization.ErrNotExist): + return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + + groupPB, err := transformGroupToPB(updatedGroup) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + + audit.GetAuditor(ctx, orgResp.ID).Log(audit.GroupUpdatedEvent, audit.GroupTarget(updatedGroup.ID)) + return connect.NewResponse(&frontierv1beta1.UpdateGroupResponse{Group: &groupPB}), nil +} + +func (h *ConnectHandler) ListGroupUsers(ctx context.Context, request *connect.Request[frontierv1beta1.ListGroupUsersRequest]) (*connect.Response[frontierv1beta1.ListGroupUsersResponse], error) { + logger := grpczap.Extract(ctx) + _, err := h.orgService.Get(ctx, request.Msg.GetOrgId()) + if err != nil { + switch { + case errors.Is(err, organization.ErrDisabled): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgDisabled) + case errors.Is(err, organization.ErrNotExist): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + + var userPBs []*frontierv1beta1.User + var rolePairPBs []*frontierv1beta1.ListGroupUsersResponse_RolePair + users, err := h.userService.ListByGroup(ctx, request.Msg.GetId(), "") + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + + for _, user := range users { + userPb, err := transformUserToPB(user) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + userPBs = append(userPBs, userPb) + } + + if request.Msg.GetWithRoles() { + for _, user := range users { + roles, err := h.policyService.ListRoles(ctx, schema.UserPrincipal, user.ID, schema.GroupNamespace, request.Msg.GetId()) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + + rolesPb := utils.Filter(utils.Map(roles, func(role role.Role) *frontierv1beta1.Role { + pb, err := transformRoleToPB(role) + if err != nil { + logger.Error("failed to transform role for group", zap.Error(err)) + return nil + } + return &pb + }), func(role *frontierv1beta1.Role) bool { + return role != nil + }) + rolePairPBs = append(rolePairPBs, &frontierv1beta1.ListGroupUsersResponse_RolePair{ + UserId: user.ID, + Roles: rolesPb, + }) + } + } + + return connect.NewResponse(&frontierv1beta1.ListGroupUsersResponse{ + Users: userPBs, + RolePairs: rolePairPBs, + }), nil +} + +func (h *ConnectHandler) AddGroupUsers(ctx context.Context, request *connect.Request[frontierv1beta1.AddGroupUsersRequest]) (*connect.Response[frontierv1beta1.AddGroupUsersResponse], error) { + _, err := h.orgService.Get(ctx, request.Msg.GetOrgId()) + if err != nil { + switch { + case errors.Is(err, organization.ErrDisabled): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgDisabled) + case errors.Is(err, organization.ErrNotExist): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + + if err := h.groupService.AddUsers(ctx, request.Msg.GetId(), request.Msg.GetUserIds()); err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + return connect.NewResponse(&frontierv1beta1.AddGroupUsersResponse{}), nil +} + +func (h *ConnectHandler) RemoveGroupUser(ctx context.Context, request *connect.Request[frontierv1beta1.RemoveGroupUserRequest]) (*connect.Response[frontierv1beta1.RemoveGroupUserResponse], error) { + _, err := h.orgService.Get(ctx, request.Msg.GetOrgId()) + if err != nil { + switch { + case errors.Is(err, organization.ErrDisabled): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgDisabled) + case errors.Is(err, organization.ErrNotExist): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + + // before deleting the user, check if the user is the only owner of the group + owners, err := h.userService.ListByGroup(ctx, request.Msg.GetId(), group.AdminRole) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + if len(owners) == 1 && owners[0].ID == request.Msg.GetUserId() { + return nil, connect.NewError(connect.CodeInvalidArgument, ErrGroupMinOwnerCount) + } + + // delete the user + if err := h.groupService.RemoveUsers(ctx, request.Msg.GetId(), []string{request.Msg.GetUserId()}); err != nil { + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + return connect.NewResponse(&frontierv1beta1.RemoveGroupUserResponse{}), nil +} + +func (h *ConnectHandler) EnableGroup(ctx context.Context, request *connect.Request[frontierv1beta1.EnableGroupRequest]) (*connect.Response[frontierv1beta1.EnableGroupResponse], error) { + _, err := h.orgService.Get(ctx, request.Msg.GetOrgId()) + if err != nil { + switch { + case errors.Is(err, organization.ErrDisabled): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgDisabled) + case errors.Is(err, organization.ErrNotExist): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + if err := h.groupService.Enable(ctx, request.Msg.GetId()); err != nil { + switch { + case errors.Is(err, group.ErrNotExist): + return nil, connect.NewError(connect.CodeNotFound, ErrGroupNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + return connect.NewResponse(&frontierv1beta1.EnableGroupResponse{}), nil +} + +func (h *ConnectHandler) DisableGroup(ctx context.Context, request *connect.Request[frontierv1beta1.DisableGroupRequest]) (*connect.Response[frontierv1beta1.DisableGroupResponse], error) { + _, err := h.orgService.Get(ctx, request.Msg.GetOrgId()) + if err != nil { + switch { + case errors.Is(err, organization.ErrDisabled): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgDisabled) + case errors.Is(err, organization.ErrNotExist): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + if err := h.groupService.Disable(ctx, request.Msg.GetId()); err != nil { + switch { + case errors.Is(err, group.ErrNotExist): + return nil, connect.NewError(connect.CodeNotFound, ErrGroupNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + return connect.NewResponse(&frontierv1beta1.DisableGroupResponse{}), nil +} + +func (h *ConnectHandler) DeleteGroup(ctx context.Context, request *connect.Request[frontierv1beta1.DeleteGroupRequest]) (*connect.Response[frontierv1beta1.DeleteGroupResponse], error) { + _, err := h.orgService.Get(ctx, request.Msg.GetOrgId()) + if err != nil { + switch { + case errors.Is(err, organization.ErrDisabled): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgDisabled) + case errors.Is(err, organization.ErrNotExist): + return nil, connect.NewError(connect.CodeNotFound, ErrOrgNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + if err := h.groupService.Delete(ctx, request.Msg.GetId()); err != nil { + switch { + case errors.Is(err, group.ErrNotExist): + return nil, connect.NewError(connect.CodeNotFound, ErrGroupNotFound) + default: + return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError) + } + } + return connect.NewResponse(&frontierv1beta1.DeleteGroupResponse{}), nil +} + func transformGroupToPB(grp group.Group) (frontierv1beta1.Group, error) { metaData, err := grp.Metadata.ToStructPB() if err != nil { diff --git a/internal/api/v1beta1connect/group_test.go b/internal/api/v1beta1connect/group_test.go index 5bb2583fd..faa1c4371 100644 --- a/internal/api/v1beta1connect/group_test.go +++ b/internal/api/v1beta1connect/group_test.go @@ -7,7 +7,11 @@ import ( "connectrpc.com/connect" "github.com/raystack/frontier/core/group" + "github.com/raystack/frontier/core/organization" + "github.com/raystack/frontier/core/role" + "github.com/raystack/frontier/core/user" "github.com/raystack/frontier/internal/api/v1beta1/mocks" + "github.com/raystack/frontier/internal/bootstrap/schema" "github.com/raystack/frontier/pkg/errors" "github.com/raystack/frontier/pkg/metadata" "github.com/raystack/frontier/pkg/utils" @@ -190,3 +194,1467 @@ func TestHandler_ListGroups(t *testing.T) { }) } } + +func TestConnectHandler_CreateGroup(t *testing.T) { + someGroupID := utils.NewString() + tests := []struct { + name string + setup func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) + request *connect.Request[frontierv1beta1.CreateGroupRequest] + want *connect.Response[frontierv1beta1.CreateGroupResponse] + wantErr bool + wantErrCode connect.Code + wantErrMsg error + }{ + { + name: "should return error if request body is nil", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + }, + request: connect.NewRequest(&frontierv1beta1.CreateGroupRequest{ + Body: nil, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInvalidArgument, + wantErrMsg: ErrBadRequest, + }, + { + name: "should return error if org does not exist", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.CreateGroupRequest{ + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "some-group", + Metadata: &structpb.Struct{}, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeNotFound, + wantErrMsg: ErrOrgNotFound, + }, + { + name: "should return error if org is disabled", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrDisabled) + }, + request: connect.NewRequest(&frontierv1beta1.CreateGroupRequest{ + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "some-group", + Metadata: &structpb.Struct{}, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeNotFound, + wantErrMsg: ErrOrgDisabled, + }, + { + name: "should return error if error in metadata validation", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + ms.EXPECT().Validate(mock.AnythingOfType("metadata.Metadata"), groupMetaSchema).Return(errors.New("some-error")) + }, + request: connect.NewRequest(&frontierv1beta1.CreateGroupRequest{ + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Metadata: &structpb.Struct{}, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInvalidArgument, + wantErrMsg: ErrBadBodyMetaSchemaError, + }, + { + name: "should return unauthenticated error if auth email in context is empty and group service return invalid user email", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + ms.EXPECT().Validate(mock.AnythingOfType("metadata.Metadata"), groupMetaSchema).Return(nil) + gs.EXPECT().Create(mock.Anything, group.Group{ + OrganizationID: testOrgID, + Title: "Test Group", + Name: "Test-Group", + Metadata: metadata.Metadata{}, + }).Return(group.Group{}, user.ErrInvalidEmail) + }, + request: connect.NewRequest(&frontierv1beta1.CreateGroupRequest{ + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Title: "Test Group", + Metadata: &structpb.Struct{}, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeUnauthenticated, + wantErrMsg: ErrUnauthenticated, + }, + { + name: "should return already exist error if group service return error conflict", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + ms.EXPECT().Validate(mock.AnythingOfType("metadata.Metadata"), groupMetaSchema).Return(nil) + gs.EXPECT().Create(mock.Anything, group.Group{ + Name: "some-group", + OrganizationID: testOrgID, + Metadata: metadata.Metadata{}, + }).Return(group.Group{}, group.ErrConflict) + }, + request: connect.NewRequest(&frontierv1beta1.CreateGroupRequest{ + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "some-group", + Metadata: &structpb.Struct{}, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeAlreadyExists, + wantErrMsg: ErrConflictRequest, + }, + { + name: "should return bad request error if name empty and group service return invalid detail error", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + ms.EXPECT().Validate(mock.AnythingOfType("metadata.Metadata"), groupMetaSchema).Return(nil) + gs.EXPECT().Create(mock.Anything, group.Group{ + Name: "some-group", + OrganizationID: testOrgID, + Metadata: metadata.Metadata{}, + }).Return(group.Group{}, group.ErrInvalidDetail) + }, + request: connect.NewRequest(&frontierv1beta1.CreateGroupRequest{ + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "some-group", + Metadata: &structpb.Struct{}, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInvalidArgument, + wantErrMsg: ErrBadRequest, + }, + { + name: "should return internal error if group service return some error", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + ms.EXPECT().Validate(mock.AnythingOfType("metadata.Metadata"), groupMetaSchema).Return(nil) + gs.EXPECT().Create(mock.Anything, group.Group{ + Name: "some-group", + OrganizationID: testOrgID, + Metadata: metadata.Metadata{}, + }).Return(group.Group{}, errors.New("test error")) + }, + request: connect.NewRequest(&frontierv1beta1.CreateGroupRequest{ + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "some-group", + Metadata: &structpb.Struct{}, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInternal, + wantErrMsg: ErrInternalServerError, + }, + { + name: "should return success if group service return nil", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + ms.EXPECT().Validate(mock.AnythingOfType("metadata.Metadata"), groupMetaSchema).Return(nil) + gs.EXPECT().Create(mock.Anything, group.Group{ + Name: "some-group", + OrganizationID: testOrgID, + Metadata: metadata.Metadata{}, + }).Return(group.Group{ + ID: someGroupID, + Name: "some-group", + OrganizationID: testOrgID, + Metadata: metadata.Metadata{}, + }, nil) + }, + request: connect.NewRequest(&frontierv1beta1.CreateGroupRequest{ + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "some-group", + Metadata: &structpb.Struct{}, + }, + }), + want: connect.NewResponse(&frontierv1beta1.CreateGroupResponse{ + Group: &frontierv1beta1.Group{ + Id: someGroupID, + Name: "some-group", + OrgId: testOrgID, + Metadata: &structpb.Struct{ + Fields: make(map[string]*structpb.Value), + }, + CreatedAt: timestamppb.New(time.Time{}), + UpdatedAt: timestamppb.New(time.Time{}), + }, + }), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockGroupSvc := new(mocks.GroupService) + mockOrgSvc := new(mocks.OrganizationService) + mockMetaSchemaSvc := new(mocks.MetaSchemaService) + if tt.setup != nil { + tt.setup(mockGroupSvc, mockMetaSchemaSvc, mockOrgSvc) + } + h := &ConnectHandler{ + groupService: mockGroupSvc, + orgService: mockOrgSvc, + metaSchemaService: mockMetaSchemaSvc, + } + got, err := h.CreateGroup(context.Background(), tt.request) + if tt.wantErr { + assert.Error(t, err) + connectErr := &connect.Error{} + assert.True(t, errors.As(err, &connectErr)) + assert.Equal(t, tt.wantErrCode, connectErr.Code()) + assert.Equal(t, tt.wantErrMsg.Error(), connectErr.Message()) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want.Msg, got.Msg) + } + }) + } +} + +func TestConnectHandler_GetGroup(t *testing.T) { + someGroupID := utils.NewString() + tests := []struct { + name string + setup func(gs *mocks.GroupService, os *mocks.OrganizationService) + request *connect.Request[frontierv1beta1.GetGroupRequest] + want *connect.Response[frontierv1beta1.GetGroupResponse] + wantErr bool + wantErrCode connect.Code + wantErrMsg error + }{ + { + name: "should return error if org does not exist", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.GetGroupRequest{ + OrgId: testOrgID, + Id: someGroupID, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeNotFound, + wantErrMsg: ErrOrgNotFound, + }, + { + name: "should return error if org is disabled", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrDisabled) + }, + request: connect.NewRequest(&frontierv1beta1.GetGroupRequest{ + OrgId: testOrgID, + Id: someGroupID, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeNotFound, + wantErrMsg: ErrOrgDisabled, + }, + { + name: "should return internal error if group service return some error", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + gs.EXPECT().Get(mock.Anything, someGroupID).Return(group.Group{}, errors.New("test error")) + }, + request: connect.NewRequest(&frontierv1beta1.GetGroupRequest{ + Id: someGroupID, + OrgId: testOrgID, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInternal, + wantErrMsg: ErrInternalServerError, + }, + { + name: "should return not found error if id is invalid", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + gs.EXPECT().Get(mock.Anything, "").Return(group.Group{}, group.ErrInvalidID) + }, + request: connect.NewRequest(&frontierv1beta1.GetGroupRequest{ + Id: "", + OrgId: testOrgID, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeNotFound, + wantErrMsg: ErrGroupNotFound, + }, + { + name: "should return not found error if group not exist", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + gs.EXPECT().Get(mock.Anything, someGroupID).Return(group.Group{}, group.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.GetGroupRequest{ + Id: someGroupID, + OrgId: testOrgID, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeNotFound, + wantErrMsg: ErrGroupNotFound, + }, + { + name: "should return success if group service return nil", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + gs.EXPECT().Get(mock.Anything, testGroupID).Return(testGroupMap[testGroupID], nil) + }, + request: connect.NewRequest(&frontierv1beta1.GetGroupRequest{ + Id: testGroupID, + OrgId: testOrgID, + }), + want: connect.NewResponse(&frontierv1beta1.GetGroupResponse{ + Group: &frontierv1beta1.Group{ + Id: testGroupID, + Name: "group-1", + OrgId: "9f256f86-31a3-11ec-8d3d-0242ac130003", + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, + }, + CreatedAt: timestamppb.New(time.Time{}), + UpdatedAt: timestamppb.New(time.Time{}), + }, + }), + wantErr: false, + }, + { + name: "should return internal error if group service return key as integer type", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + gs.EXPECT().Get(mock.Anything, testGroupID).Return(group.Group{ + Metadata: metadata.Metadata{ + "key": map[int]any{}, + }, + }, nil) + }, + request: connect.NewRequest(&frontierv1beta1.GetGroupRequest{ + Id: testGroupID, + OrgId: testOrgID, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInternal, + wantErrMsg: ErrInternalServerError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockOrgSvc := new(mocks.OrganizationService) + mockGroupSvc := new(mocks.GroupService) + if tt.setup != nil { + tt.setup(mockGroupSvc, mockOrgSvc) + } + h := &ConnectHandler{ + groupService: mockGroupSvc, + orgService: mockOrgSvc, + } + got, err := h.GetGroup(context.Background(), tt.request) + if tt.wantErr { + assert.Error(t, err) + connectErr := &connect.Error{} + assert.True(t, errors.As(err, &connectErr)) + assert.Equal(t, tt.wantErrCode, connectErr.Code()) + assert.Equal(t, tt.wantErrMsg.Error(), connectErr.Message()) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want.Msg, got.Msg) + } + }) + } +} + +func TestConnectHandler_UpdateGroup(t *testing.T) { + someGroupID := utils.NewString() + tests := []struct { + name string + setup func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) + request *connect.Request[frontierv1beta1.UpdateGroupRequest] + want *connect.Response[frontierv1beta1.UpdateGroupResponse] + wantErr bool + wantErrCode connect.Code + wantErrMsg error + }{ + { + name: "should return bad request error if body is empty", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + }, + request: connect.NewRequest(&frontierv1beta1.UpdateGroupRequest{ + Id: someGroupID, + Body: nil, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInvalidArgument, + wantErrMsg: ErrBadRequest, + }, + { + name: "should return error if org does not exist", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.UpdateGroupRequest{ + Id: someGroupID, + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "new-group", + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeNotFound, + wantErrMsg: ErrOrgNotFound, + }, + { + name: "should return org is disabled", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrDisabled) + }, + request: connect.NewRequest(&frontierv1beta1.UpdateGroupRequest{ + Id: someGroupID, + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "new-group", + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeNotFound, + wantErrMsg: ErrOrgDisabled, + }, + { + name: "should return error if error in metadata validation", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + ms.EXPECT().Validate(mock.AnythingOfType("metadata.Metadata"), groupMetaSchema).Return(errors.New("some-error")) + }, + request: connect.NewRequest(&frontierv1beta1.UpdateGroupRequest{ + Id: someGroupID, + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "new-group", + Metadata: &structpb.Struct{}, + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInvalidArgument, + wantErrMsg: ErrBadBodyMetaSchemaError, + }, + { + name: "should return not found error if group id is not uuid (slug) and does not exist", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + ms.EXPECT().Validate(mock.AnythingOfType("metadata.Metadata"), groupMetaSchema).Return(nil) + gs.EXPECT().Update(mock.Anything, group.Group{ + ID: "some-id", + Name: "some-id", + OrganizationID: testOrgID, + Metadata: metadata.Metadata{}, + }).Return(group.Group{}, group.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.UpdateGroupRequest{ + Id: "some-id", + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "some-id", + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeNotFound, + wantErrMsg: ErrGroupNotFound, + }, + { + name: "should return not found error if group id is uuid and does not exist", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + ms.EXPECT().Validate(mock.AnythingOfType("metadata.Metadata"), groupMetaSchema).Return(nil) + gs.EXPECT().Update(mock.Anything, group.Group{ + ID: someGroupID, + Name: "new-group", + OrganizationID: testOrgID, + Metadata: metadata.Metadata{}, + }).Return(group.Group{}, group.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.UpdateGroupRequest{ + Id: someGroupID, + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "new-group", + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeNotFound, + wantErrMsg: ErrGroupNotFound, + }, + { + name: "should return already exist error if group service return error conflict", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + ms.EXPECT().Validate(mock.AnythingOfType("metadata.Metadata"), groupMetaSchema).Return(nil) + gs.EXPECT().Update(mock.Anything, group.Group{ + ID: someGroupID, + Name: "new-group", + OrganizationID: testOrgID, + Metadata: metadata.Metadata{}, + }).Return(group.Group{}, group.ErrConflict) + }, + request: connect.NewRequest(&frontierv1beta1.UpdateGroupRequest{ + Id: someGroupID, + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "new-group", + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeAlreadyExists, + wantErrMsg: ErrConflictRequest, + }, + { + name: "should return bad request error if name is empty", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + ms.EXPECT().Validate(mock.AnythingOfType("metadata.Metadata"), groupMetaSchema).Return(nil) + gs.EXPECT().Update(mock.Anything, group.Group{ + ID: someGroupID, + Name: "new-group", + OrganizationID: testOrgID, + Metadata: metadata.Metadata{}, + }).Return(group.Group{}, group.ErrInvalidDetail) + }, + request: connect.NewRequest(&frontierv1beta1.UpdateGroupRequest{ + Id: someGroupID, + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "new-group", + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInvalidArgument, + wantErrMsg: ErrBadRequest, + }, + { + name: "should return internal error if group service return some error", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + ms.EXPECT().Validate(mock.AnythingOfType("metadata.Metadata"), groupMetaSchema).Return(nil) + gs.EXPECT().Update(mock.Anything, group.Group{ + ID: someGroupID, + Name: "new-group", + OrganizationID: testOrgID, + Metadata: metadata.Metadata{}, + }).Return(group.Group{}, errors.New("test error")) + }, + request: connect.NewRequest(&frontierv1beta1.UpdateGroupRequest{ + Id: someGroupID, + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "new-group", + }, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeInternal, + wantErrMsg: ErrInternalServerError, + }, + { + name: "should return success if updated by id and group service return nil error", + setup: func(gs *mocks.GroupService, ms *mocks.MetaSchemaService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + ms.EXPECT().Validate(mock.AnythingOfType("metadata.Metadata"), groupMetaSchema).Return(nil) + gs.EXPECT().Update(mock.Anything, group.Group{ + ID: someGroupID, + Name: "new-group", + OrganizationID: testOrgID, + Metadata: metadata.Metadata{}, + }).Return(group.Group{ + ID: someGroupID, + Name: "new-group", + OrganizationID: testOrgID, + Metadata: metadata.Metadata{}, + }, nil) + }, + request: connect.NewRequest(&frontierv1beta1.UpdateGroupRequest{ + Id: someGroupID, + OrgId: testOrgID, + Body: &frontierv1beta1.GroupRequestBody{ + Name: "new-group", + }, + }), + want: connect.NewResponse(&frontierv1beta1.UpdateGroupResponse{ + Group: &frontierv1beta1.Group{ + Id: someGroupID, + Name: "new-group", + OrgId: testOrgID, + Metadata: &structpb.Struct{ + Fields: make(map[string]*structpb.Value), + }, + CreatedAt: timestamppb.New(time.Time{}), + UpdatedAt: timestamppb.New(time.Time{}), + }, + }), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockGroupSvc := new(mocks.GroupService) + mockOrgSvc := new(mocks.OrganizationService) + mockMetaSchemaSvc := new(mocks.MetaSchemaService) + if tt.setup != nil { + tt.setup(mockGroupSvc, mockMetaSchemaSvc, mockOrgSvc) + } + h := &ConnectHandler{ + groupService: mockGroupSvc, + orgService: mockOrgSvc, + metaSchemaService: mockMetaSchemaSvc, + } + got, err := h.UpdateGroup(context.Background(), tt.request) + if tt.wantErr { + assert.Error(t, err) + connectErr := &connect.Error{} + assert.True(t, errors.As(err, &connectErr)) + assert.Equal(t, tt.wantErrCode, connectErr.Code()) + assert.Equal(t, tt.wantErrMsg.Error(), connectErr.Message()) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want.Msg, got.Msg) + } + }) + } +} + +func TestConnectHandler_ListOrganizationGroups(t *testing.T) { + var validGroupResponseWithUser = &frontierv1beta1.Group{ + Id: testGroupID, + Name: "group-1", + OrgId: "9f256f86-31a3-11ec-8d3d-0242ac130003", + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + }, + }, + CreatedAt: timestamppb.New(time.Time{}), + UpdatedAt: timestamppb.New(time.Time{}), + Users: []*frontierv1beta1.User{ + { + Id: testUserID, + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{}, + }, + CreatedAt: timestamppb.New(time.Time{}), + UpdatedAt: timestamppb.New(time.Time{}), + }, + }, + } + + tests := []struct { + name string + setup func(gs *mocks.GroupService, os *mocks.OrganizationService, us *mocks.UserService) + request *connect.Request[frontierv1beta1.ListOrganizationGroupsRequest] + want *connect.Response[frontierv1beta1.ListOrganizationGroupsResponse] + wantErr bool + wantErrCode connect.Code + wantErrMsg error + }{ + { + name: "should return error if org does not exist", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, us *mocks.UserService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.ListOrganizationGroupsRequest{ + OrgId: testOrgID, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeNotFound, + wantErrMsg: ErrOrgNotFound, + }, + { + name: "should return error if org is disabled", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, us *mocks.UserService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrDisabled) + }, + request: connect.NewRequest(&frontierv1beta1.ListOrganizationGroupsRequest{ + OrgId: testOrgID, + }), + want: nil, + wantErr: true, + wantErrCode: connect.CodeNotFound, + wantErrMsg: ErrOrgDisabled, + }, + { + name: "should return empty groups list if organization with valid uuid is not found", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, us *mocks.UserService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + gs.EXPECT().List(mock.Anything, group.Filter{ + OrganizationID: testOrgID, + }).Return([]group.Group{}, nil) + }, + request: connect.NewRequest(&frontierv1beta1.ListOrganizationGroupsRequest{ + OrgId: testOrgID, + }), + want: connect.NewResponse(&frontierv1beta1.ListOrganizationGroupsResponse{ + Groups: nil, + }), + wantErr: false, + }, + { + name: "should return success if list organization groups and group service return nil error", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, us *mocks.UserService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + var testGroupList []group.Group + for _, u := range testGroupMap { + testGroupList = append(testGroupList, u) + } + gs.EXPECT().List(mock.Anything, group.Filter{ + OrganizationID: testOrgID, + }).Return(testGroupList, nil) + us.EXPECT().ListByGroup(mock.Anything, testGroupID, "").Return([]user.User{ + { + ID: testUserID, + Metadata: map[string]any{}, + }, + }, nil) + }, + request: connect.NewRequest(&frontierv1beta1.ListOrganizationGroupsRequest{ + OrgId: testOrgID, + WithMembers: true, + }), + want: connect.NewResponse(&frontierv1beta1.ListOrganizationGroupsResponse{ + Groups: []*frontierv1beta1.Group{ + validGroupResponseWithUser, + }, + }), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockOrgSvc := new(mocks.OrganizationService) + mockGroupSvc := new(mocks.GroupService) + mockUserSvc := new(mocks.UserService) + if tt.setup != nil { + tt.setup(mockGroupSvc, mockOrgSvc, mockUserSvc) + } + h := &ConnectHandler{ + groupService: mockGroupSvc, + orgService: mockOrgSvc, + userService: mockUserSvc, + } + got, err := h.ListOrganizationGroups(context.Background(), tt.request) + if tt.wantErr { + assert.Error(t, err) + connectErr := &connect.Error{} + assert.True(t, errors.As(err, &connectErr)) + assert.Equal(t, tt.wantErrCode, connectErr.Code()) + assert.Equal(t, tt.wantErrMsg.Error(), connectErr.Message()) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want.Msg, got.Msg) + } + }) + } +} + +func TestConnectHandler_ListGroupUsers(t *testing.T) { + someGroupID := utils.NewString() + tests := []struct { + name string + setup func(gs *mocks.GroupService, us *mocks.UserService, os *mocks.OrganizationService, ps *mocks.PolicyService) + request *connect.Request[frontierv1beta1.ListGroupUsersRequest] + want *connect.Response[frontierv1beta1.ListGroupUsersResponse] + wantErr error + }{ + { + name: "should return error if org does not exist", + setup: func(gs *mocks.GroupService, us *mocks.UserService, os *mocks.OrganizationService, ps *mocks.PolicyService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.ListGroupUsersRequest{ + Id: someGroupID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrOrgNotFound), + }, + { + name: "should error if org is disabled", + setup: func(gs *mocks.GroupService, us *mocks.UserService, os *mocks.OrganizationService, ps *mocks.PolicyService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrDisabled) + }, + request: connect.NewRequest(&frontierv1beta1.ListGroupUsersRequest{ + Id: someGroupID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrOrgDisabled), + }, + { + name: "should return internal server error if error in listing group users", + setup: func(gs *mocks.GroupService, us *mocks.UserService, os *mocks.OrganizationService, ps *mocks.PolicyService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + us.EXPECT().ListByGroup(mock.Anything, someGroupID, "").Return(nil, errors.New("some error")) + }, + request: connect.NewRequest(&frontierv1beta1.ListGroupUsersRequest{ + Id: someGroupID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeInternal, ErrInternalServerError), + }, + { + name: "should return error if metadata transformation fails in list of group users", + setup: func(gs *mocks.GroupService, us *mocks.UserService, os *mocks.OrganizationService, ps *mocks.PolicyService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + testUserList := []user.User{ + { + Metadata: metadata.Metadata{ + "key": map[int]string{}, + }, + }, + } + + us.EXPECT().ListByGroup(mock.Anything, someGroupID, "").Return(testUserList, nil) + }, + request: connect.NewRequest(&frontierv1beta1.ListGroupUsersRequest{ + Id: someGroupID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeInternal, ErrInternalServerError), + }, + { + name: "should return success if list group users and group service return nil error", + setup: func(gs *mocks.GroupService, us *mocks.UserService, os *mocks.OrganizationService, ps *mocks.PolicyService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + var testUserList []user.User + for _, u := range testUserMap { + testUserList = append(testUserList, u) + } + us.EXPECT().ListByGroup(mock.Anything, someGroupID, "").Return(testUserList, nil) + }, + request: connect.NewRequest(&frontierv1beta1.ListGroupUsersRequest{ + Id: someGroupID, + OrgId: testOrgID, + }), + want: connect.NewResponse(&frontierv1beta1.ListGroupUsersResponse{ + Users: []*frontierv1beta1.User{ + { + Id: "9f256f86-31a3-11ec-8d3d-0242ac130003", + Title: "User 1", + Name: "user1", + Email: "test@test.com", + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + "age": structpb.NewNumberValue(21), + "intern": structpb.NewBoolValue(true), + }, + }, + CreatedAt: timestamppb.New(time.Time{}), + UpdatedAt: timestamppb.New(time.Time{}), + }, + }, + }), + wantErr: nil, + }, + { + name: "should return error if policy service fails when WithRoles is true", + setup: func(gs *mocks.GroupService, us *mocks.UserService, os *mocks.OrganizationService, ps *mocks.PolicyService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + var testUserList []user.User + for _, u := range testUserMap { + testUserList = append(testUserList, u) + } + us.EXPECT().ListByGroup(mock.Anything, someGroupID, "").Return(testUserList, nil) + ps.EXPECT().ListRoles(mock.Anything, schema.UserPrincipal, "9f256f86-31a3-11ec-8d3d-0242ac130003", schema.GroupNamespace, someGroupID).Return(nil, errors.New("policy error")) + }, + request: connect.NewRequest(&frontierv1beta1.ListGroupUsersRequest{ + Id: someGroupID, + OrgId: testOrgID, + WithRoles: true, + }), + want: nil, + wantErr: connect.NewError(connect.CodeInternal, ErrInternalServerError), + }, + { + name: "should return success with roles when WithRoles is true", + setup: func(gs *mocks.GroupService, us *mocks.UserService, os *mocks.OrganizationService, ps *mocks.PolicyService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + var testUserList []user.User + for _, u := range testUserMap { + testUserList = append(testUserList, u) + } + us.EXPECT().ListByGroup(mock.Anything, someGroupID, "").Return(testUserList, nil) + + testRoles := []role.Role{ + { + ID: "test-role-id", + Name: "admin", + Title: "Administrator", + Metadata: metadata.Metadata{}, + }, + } + ps.EXPECT().ListRoles(mock.Anything, schema.UserPrincipal, "9f256f86-31a3-11ec-8d3d-0242ac130003", schema.GroupNamespace, someGroupID).Return(testRoles, nil) + }, + request: connect.NewRequest(&frontierv1beta1.ListGroupUsersRequest{ + Id: someGroupID, + OrgId: testOrgID, + WithRoles: true, + }), + want: connect.NewResponse(&frontierv1beta1.ListGroupUsersResponse{ + Users: []*frontierv1beta1.User{ + { + Id: "9f256f86-31a3-11ec-8d3d-0242ac130003", + Title: "User 1", + Name: "user1", + Email: "test@test.com", + Metadata: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "foo": structpb.NewStringValue("bar"), + "age": structpb.NewNumberValue(21), + "intern": structpb.NewBoolValue(true), + }, + }, + CreatedAt: timestamppb.New(time.Time{}), + UpdatedAt: timestamppb.New(time.Time{}), + }, + }, + RolePairs: []*frontierv1beta1.ListGroupUsersResponse_RolePair{ + { + UserId: "9f256f86-31a3-11ec-8d3d-0242ac130003", + Roles: []*frontierv1beta1.Role{ + { + Id: "test-role-id", + Name: "admin", + Title: "Administrator", + Metadata: &structpb.Struct{Fields: map[string]*structpb.Value{}}, + CreatedAt: timestamppb.New(time.Time{}), + UpdatedAt: timestamppb.New(time.Time{}), + }, + }, + }, + }, + }), + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockGroupSvc := new(mocks.GroupService) + mockUserSvc := new(mocks.UserService) + mockOrgSvc := new(mocks.OrganizationService) + mockPolicySvc := new(mocks.PolicyService) + if tt.setup != nil { + tt.setup(mockGroupSvc, mockUserSvc, mockOrgSvc, mockPolicySvc) + } + h := ConnectHandler{ + groupService: mockGroupSvc, + userService: mockUserSvc, + orgService: mockOrgSvc, + policyService: mockPolicySvc, + } + got, err := h.ListGroupUsers(context.Background(), tt.request) + if tt.wantErr != nil { + assert.Error(t, err) + assert.Equal(t, tt.wantErr.(*connect.Error).Code(), err.(*connect.Error).Code()) + assert.Equal(t, tt.wantErr.(*connect.Error).Message(), err.(*connect.Error).Message()) + } else { + assert.NoError(t, err) + assert.EqualValues(t, tt.want, got) + } + }) + } +} + +func TestConnectHandler_AddGroupUsers(t *testing.T) { + someGroupID := utils.NewString() + someUserID := utils.NewString() + tests := []struct { + name string + setup func(gs *mocks.GroupService, os *mocks.OrganizationService) + request *connect.Request[frontierv1beta1.AddGroupUsersRequest] + want *connect.Response[frontierv1beta1.AddGroupUsersResponse] + wantErr error + }{ + { + name: "should return error if org does not exist", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.AddGroupUsersRequest{ + Id: someGroupID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrOrgNotFound), + }, + { + name: "should return error if org is disabled", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrDisabled) + }, + request: connect.NewRequest(&frontierv1beta1.AddGroupUsersRequest{ + Id: someGroupID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrOrgDisabled), + }, + { + name: "should return internal server error if error in adding group users", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + gs.EXPECT().AddUsers(mock.Anything, someGroupID, []string{someUserID}).Return(errors.New("some error")) + }, + request: connect.NewRequest(&frontierv1beta1.AddGroupUsersRequest{ + Id: someGroupID, + OrgId: testOrgID, + UserIds: []string{someUserID}, + }), + want: nil, + wantErr: connect.NewError(connect.CodeInternal, ErrInternalServerError), + }, + { + name: "should return success if add group users and group service return nil error", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(testOrgMap[testOrgID], nil) + gs.EXPECT().AddUsers(mock.Anything, someGroupID, []string{someUserID}).Return(nil) + }, + request: connect.NewRequest(&frontierv1beta1.AddGroupUsersRequest{ + Id: someGroupID, + OrgId: testOrgID, + UserIds: []string{someUserID}, + }), + want: connect.NewResponse(&frontierv1beta1.AddGroupUsersResponse{}), + wantErr: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockGroupSvc := new(mocks.GroupService) + mockOrgSvc := new(mocks.OrganizationService) + if tt.setup != nil { + tt.setup(mockGroupSvc, mockOrgSvc) + } + h := ConnectHandler{ + groupService: mockGroupSvc, + orgService: mockOrgSvc, + } + got, err := h.AddGroupUsers(context.Background(), tt.request) + if tt.wantErr != nil { + assert.Error(t, err) + assert.Equal(t, tt.wantErr.(*connect.Error).Code(), err.(*connect.Error).Code()) + assert.Equal(t, tt.wantErr.(*connect.Error).Message(), err.(*connect.Error).Message()) + } else { + assert.NoError(t, err) + assert.EqualValues(t, tt.want, got) + } + }) + } +} + +func TestConnectHandler_RemoveGroupUser(t *testing.T) { + randomID := utils.NewString() + tests := []struct { + name string + setup func(gs *mocks.GroupService, os *mocks.OrganizationService, us *mocks.UserService) + request *connect.Request[frontierv1beta1.RemoveGroupUserRequest] + want *connect.Response[frontierv1beta1.RemoveGroupUserResponse] + wantErr error + }{ + { + name: "should return error if organization does not exist", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, us *mocks.UserService) { + os.EXPECT().Get(mock.Anything, randomID).Return(organization.Organization{}, organization.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.RemoveGroupUserRequest{ + Id: randomID, + OrgId: randomID, + UserId: randomID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrOrgNotFound), + }, + { + name: "should return error if organization is disabled", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, us *mocks.UserService) { + os.EXPECT().Get(mock.Anything, randomID).Return(organization.Organization{}, organization.ErrDisabled) + }, + request: connect.NewRequest(&frontierv1beta1.RemoveGroupUserRequest{ + Id: randomID, + OrgId: randomID, + UserId: randomID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrOrgDisabled), + }, + { + name: "should return error if user service fails when checking owners", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, us *mocks.UserService) { + os.EXPECT().Get(mock.Anything, randomID).Return(organization.Organization{ID: randomID}, nil) + us.EXPECT().ListByGroup(mock.Anything, randomID, group.AdminRole).Return([]user.User{}, errors.New("user service error")) + }, + request: connect.NewRequest(&frontierv1beta1.RemoveGroupUserRequest{ + Id: randomID, + OrgId: randomID, + UserId: randomID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeInternal, ErrInternalServerError), + }, + { + name: "should return error if user is the only admin", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, us *mocks.UserService) { + os.EXPECT().Get(mock.Anything, randomID).Return(organization.Organization{ID: randomID}, nil) + us.EXPECT().ListByGroup(mock.Anything, randomID, group.AdminRole).Return([]user.User{{ID: randomID}}, nil) + }, + request: connect.NewRequest(&frontierv1beta1.RemoveGroupUserRequest{ + Id: randomID, + OrgId: randomID, + UserId: randomID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeInvalidArgument, ErrGroupMinOwnerCount), + }, + { + name: "should return error if group service fails to remove user", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, us *mocks.UserService) { + os.EXPECT().Get(mock.Anything, randomID).Return(organization.Organization{ID: randomID}, nil) + us.EXPECT().ListByGroup(mock.Anything, randomID, group.AdminRole).Return([]user.User{{ID: "other-admin"}, {ID: randomID}}, nil) + gs.EXPECT().RemoveUsers(mock.Anything, randomID, []string{randomID}).Return(errors.New("group service error")) + }, + request: connect.NewRequest(&frontierv1beta1.RemoveGroupUserRequest{ + Id: randomID, + OrgId: randomID, + UserId: randomID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeInternal, ErrInternalServerError), + }, + { + name: "should remove user successfully", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService, us *mocks.UserService) { + os.EXPECT().Get(mock.Anything, randomID).Return(organization.Organization{ID: randomID}, nil) + us.EXPECT().ListByGroup(mock.Anything, randomID, group.AdminRole).Return([]user.User{{ID: "other-admin"}, {ID: randomID}}, nil) + gs.EXPECT().RemoveUsers(mock.Anything, randomID, []string{randomID}).Return(nil) + }, + request: connect.NewRequest(&frontierv1beta1.RemoveGroupUserRequest{ + Id: randomID, + OrgId: randomID, + UserId: randomID, + }), + want: connect.NewResponse(&frontierv1beta1.RemoveGroupUserResponse{}), + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockGroupSvc := new(mocks.GroupService) + mockOrgSvc := new(mocks.OrganizationService) + mockUserSvc := new(mocks.UserService) + if tt.setup != nil { + tt.setup(mockGroupSvc, mockOrgSvc, mockUserSvc) + } + h := ConnectHandler{ + groupService: mockGroupSvc, + orgService: mockOrgSvc, + userService: mockUserSvc, + } + got, err := h.RemoveGroupUser(context.Background(), tt.request) + if tt.wantErr != nil { + assert.Error(t, err) + assert.Equal(t, tt.wantErr.(*connect.Error).Code(), err.(*connect.Error).Code()) + assert.Equal(t, tt.wantErr.(*connect.Error).Message(), err.(*connect.Error).Message()) + } else { + assert.NoError(t, err) + assert.EqualValues(t, tt.want, got) + } + }) + } +} + +func TestConnectHandler_EnableGroup(t *testing.T) { + randomID := utils.NewString() + tests := []struct { + name string + setup func(gs *mocks.GroupService, os *mocks.OrganizationService) + request *connect.Request[frontierv1beta1.EnableGroupRequest] + want *connect.Response[frontierv1beta1.EnableGroupResponse] + wantErr error + }{ + { + name: "should return error if organization does not exist", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.EnableGroupRequest{ + Id: randomID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrOrgNotFound), + }, + { + name: "should return error if organization is disabled", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrDisabled) + }, + request: connect.NewRequest(&frontierv1beta1.EnableGroupRequest{ + Id: randomID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrOrgDisabled), + }, + { + name: "should return error if group does not exist", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{ID: testOrgID}, nil) + gs.EXPECT().Enable(mock.Anything, randomID).Return(group.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.EnableGroupRequest{ + Id: randomID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrGroupNotFound), + }, + { + name: "should enable group successfully", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{ID: testOrgID}, nil) + gs.EXPECT().Enable(mock.Anything, randomID).Return(nil) + }, + request: connect.NewRequest(&frontierv1beta1.EnableGroupRequest{ + Id: randomID, + OrgId: testOrgID, + }), + want: connect.NewResponse(&frontierv1beta1.EnableGroupResponse{}), + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockGroupSvc := new(mocks.GroupService) + mockOrgSvc := new(mocks.OrganizationService) + if tt.setup != nil { + tt.setup(mockGroupSvc, mockOrgSvc) + } + h := ConnectHandler{ + groupService: mockGroupSvc, + orgService: mockOrgSvc, + } + got, err := h.EnableGroup(context.Background(), tt.request) + if tt.wantErr != nil { + assert.Error(t, err) + assert.Equal(t, tt.wantErr.(*connect.Error).Code(), err.(*connect.Error).Code()) + assert.Equal(t, tt.wantErr.(*connect.Error).Message(), err.(*connect.Error).Message()) + } else { + assert.NoError(t, err) + assert.EqualValues(t, tt.want, got) + } + }) + } +} + +func TestConnectHandler_DisableGroup(t *testing.T) { + randomID := utils.NewString() + tests := []struct { + name string + setup func(gs *mocks.GroupService, os *mocks.OrganizationService) + request *connect.Request[frontierv1beta1.DisableGroupRequest] + want *connect.Response[frontierv1beta1.DisableGroupResponse] + wantErr error + }{ + { + name: "should return error if organization does not exist", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.DisableGroupRequest{ + Id: randomID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrOrgNotFound), + }, + { + name: "should return error if organization is disabled", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrDisabled) + }, + request: connect.NewRequest(&frontierv1beta1.DisableGroupRequest{ + Id: randomID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrOrgDisabled), + }, + { + name: "should return error if group does not exist", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{ID: testOrgID}, nil) + gs.EXPECT().Disable(mock.Anything, randomID).Return(group.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.DisableGroupRequest{ + Id: randomID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrGroupNotFound), + }, + { + name: "should disable group successfully", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{ID: testOrgID}, nil) + gs.EXPECT().Disable(mock.Anything, randomID).Return(nil) + }, + request: connect.NewRequest(&frontierv1beta1.DisableGroupRequest{ + Id: randomID, + OrgId: testOrgID, + }), + want: connect.NewResponse(&frontierv1beta1.DisableGroupResponse{}), + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockGroupSvc := new(mocks.GroupService) + mockOrgSvc := new(mocks.OrganizationService) + if tt.setup != nil { + tt.setup(mockGroupSvc, mockOrgSvc) + } + h := ConnectHandler{ + groupService: mockGroupSvc, + orgService: mockOrgSvc, + } + got, err := h.DisableGroup(context.Background(), tt.request) + if tt.wantErr != nil { + assert.Error(t, err) + assert.Equal(t, tt.wantErr.(*connect.Error).Code(), err.(*connect.Error).Code()) + assert.Equal(t, tt.wantErr.(*connect.Error).Message(), err.(*connect.Error).Message()) + } else { + assert.NoError(t, err) + assert.EqualValues(t, tt.want, got) + } + }) + } +} + +func TestConnectHandler_DeleteGroup(t *testing.T) { + randomID := utils.NewString() + tests := []struct { + name string + setup func(gs *mocks.GroupService, os *mocks.OrganizationService) + request *connect.Request[frontierv1beta1.DeleteGroupRequest] + want *connect.Response[frontierv1beta1.DeleteGroupResponse] + wantErr error + }{ + { + name: "should return error if organization does not exist", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.DeleteGroupRequest{ + Id: randomID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrOrgNotFound), + }, + { + name: "should return error if organization is disabled", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{}, organization.ErrDisabled) + }, + request: connect.NewRequest(&frontierv1beta1.DeleteGroupRequest{ + Id: randomID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrOrgDisabled), + }, + { + name: "should return error if group does not exist", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{ID: testOrgID}, nil) + gs.EXPECT().Delete(mock.Anything, randomID).Return(group.ErrNotExist) + }, + request: connect.NewRequest(&frontierv1beta1.DeleteGroupRequest{ + Id: randomID, + OrgId: testOrgID, + }), + want: nil, + wantErr: connect.NewError(connect.CodeNotFound, ErrGroupNotFound), + }, + { + name: "should delete group successfully", + setup: func(gs *mocks.GroupService, os *mocks.OrganizationService) { + os.EXPECT().Get(mock.Anything, testOrgID).Return(organization.Organization{ID: testOrgID}, nil) + gs.EXPECT().Delete(mock.Anything, randomID).Return(nil) + }, + request: connect.NewRequest(&frontierv1beta1.DeleteGroupRequest{ + Id: randomID, + OrgId: testOrgID, + }), + want: connect.NewResponse(&frontierv1beta1.DeleteGroupResponse{}), + wantErr: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockGroupSvc := new(mocks.GroupService) + mockOrgSvc := new(mocks.OrganizationService) + if tt.setup != nil { + tt.setup(mockGroupSvc, mockOrgSvc) + } + h := ConnectHandler{ + groupService: mockGroupSvc, + orgService: mockOrgSvc, + } + got, err := h.DeleteGroup(context.Background(), tt.request) + if tt.wantErr != nil { + assert.Error(t, err) + assert.Equal(t, tt.wantErr.(*connect.Error).Code(), err.(*connect.Error).Code()) + assert.Equal(t, tt.wantErr.(*connect.Error).Message(), err.(*connect.Error).Message()) + } else { + assert.NoError(t, err) + assert.EqualValues(t, tt.want, got) + } + }) + } +} diff --git a/internal/api/v1beta1connect/metaschema.go b/internal/api/v1beta1connect/metaschema.go index f16fd297f..a37a4baba 100644 --- a/internal/api/v1beta1connect/metaschema.go +++ b/internal/api/v1beta1connect/metaschema.go @@ -4,4 +4,5 @@ var ( roleMetaSchema = "role" prospectMetaSchema = "prospect" userMetaSchema = "user" + groupMetaSchema = "group" )