Path: blob/main/components/public-api-server/pkg/apiv1/team_test.go
2499 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package apiv156import (7"context"8"net/http"9"net/http/httptest"10"testing"1112"github.com/bufbuild/connect-go"13"github.com/gitpod-io/gitpod/components/public-api/go/config"14v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"15"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"16protocol "github.com/gitpod-io/gitpod/gitpod-protocol"17"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"18"github.com/gitpod-io/gitpod/public-api-server/pkg/jws"19"github.com/gitpod-io/gitpod/public-api-server/pkg/jws/jwstest"20"github.com/golang/mock/gomock"21"github.com/google/go-cmp/cmp"22"github.com/google/uuid"23"github.com/sourcegraph/jsonrpc2"24"github.com/stretchr/testify/require"25"google.golang.org/protobuf/testing/protocmp"26)2728func TestTeamsService_CreateTeam(t *testing.T) {2930var (31name = "Shiny New Team"32id = uuid.New().String()33)3435t.Run("returns invalid argument when name is empty", func(t *testing.T) {36ctx := context.Background()37_, client := setupTeamService(t)3839_, err := client.CreateTeam(ctx, connect.NewRequest(&v1.CreateTeamRequest{Name: ""}))40require.Error(t, err)41require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))42})4344t.Run("returns invalid request when server returns invalid request", func(t *testing.T) {45ctx := context.Background()46serverMock, client := setupTeamService(t)4748serverMock.EXPECT().CreateTeam(gomock.Any(), name).Return(nil, &jsonrpc2.Error{49Code: 400,50Message: "invalid request",51})5253_, err := client.CreateTeam(ctx, connect.NewRequest(&v1.CreateTeamRequest{Name: name}))54require.Error(t, err)55require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))56})5758t.Run("returns team with members and invite", func(t *testing.T) {59teamMembers := []*protocol.TeamMemberInfo{60newTeamMember(&protocol.TeamMemberInfo{61FullName: "Alice Alice",62Role: protocol.TeamMember_Owner,63}),64newTeamMember(&protocol.TeamMemberInfo{65FullName: "Bob Bob",66Role: protocol.TeamMember_Member,67}),68}69inviteID := uuid.New().String()70invite := &protocol.TeamMembershipInvite{71ID: inviteID,72TeamID: id,73}74team := newTeam(&protocol.Team{75ID: id,76})7778serverMock, client := setupTeamService(t)7980serverMock.EXPECT().CreateTeam(gomock.Any(), name).Return(team, nil)81serverMock.EXPECT().GetTeamMembers(gomock.Any(), id).Return(teamMembers, nil)82serverMock.EXPECT().GetGenericInvite(gomock.Any(), id).Return(&protocol.TeamMembershipInvite{83ID: inviteID,84TeamID: id,85}, nil)8687response, err := client.CreateTeam(context.Background(), connect.NewRequest(&v1.CreateTeamRequest{Name: name}))88require.NoError(t, err)8990requireEqualProto(t, &v1.CreateTeamResponse{91Team: teamToAPIResponse(team, teamMembers, invite),92}, response.Msg)93})9495t.Run("returns team with members and no invite", func(t *testing.T) {96teamMembers := []*protocol.TeamMemberInfo{97newTeamMember(&protocol.TeamMemberInfo{98FullName: "Alice Alice",99Role: protocol.TeamMember_Owner,100}),101newTeamMember(&protocol.TeamMemberInfo{102FullName: "Bob Bob",103Role: protocol.TeamMember_Member,104}),105}106team := newTeam(&protocol.Team{107ID: id,108})109110serverMock, client := setupTeamService(t)111112serverMock.EXPECT().CreateTeam(gomock.Any(), name).Return(team, nil)113serverMock.EXPECT().GetTeamMembers(gomock.Any(), id).Return(teamMembers, nil)114serverMock.EXPECT().GetGenericInvite(gomock.Any(), id).Return(nil, &jsonrpc2.Error{Code: 404, Message: "not found"})115116response, err := client.CreateTeam(context.Background(), connect.NewRequest(&v1.CreateTeamRequest{Name: name}))117require.NoError(t, err)118119requireEqualProto(t, &v1.CreateTeamResponse{120Team: teamToAPIResponse(team, teamMembers, nil),121}, response.Msg)122})123}124125func TestTeamsService_ListTeams(t *testing.T) {126t.Run("returns teams with members and invite", func(t *testing.T) {127ctx := context.Background()128serverMock, client := setupTeamService(t)129130teamMembers := []*protocol.TeamMemberInfo{131newTeamMember(&protocol.TeamMemberInfo{132FullName: "Alice Alice",133Role: protocol.TeamMember_Owner,134}),135newTeamMember(&protocol.TeamMemberInfo{136FullName: "Bob Bob",137Role: protocol.TeamMember_Member,138}),139}140teams := []*protocol.Team{141newTeam(&protocol.Team{142Name: "Team A",143}),144newTeam(&protocol.Team{145Name: "Team B",146}),147}148inviteID := uuid.New().String()149invite := &protocol.TeamMembershipInvite{150ID: inviteID,151TeamID: teams[1].ID,152}153154serverMock.EXPECT().GetTeams(gomock.Any()).Return(teams, nil)155156// Mocks for populating team A details157serverMock.EXPECT().GetTeamMembers(gomock.Any(), teams[0].ID).Return(teamMembers, nil)158serverMock.EXPECT().GetGenericInvite(gomock.Any(), teams[0].ID).Return(&protocol.TeamMembershipInvite{159ID: inviteID,160TeamID: teams[0].ID,161}, nil)162// Mock for populating team B details163serverMock.EXPECT().GetTeamMembers(gomock.Any(), teams[1].ID).Return(teamMembers, nil)164serverMock.EXPECT().GetGenericInvite(gomock.Any(), teams[1].ID).Return(invite, nil)165166response, err := client.ListTeams(ctx, connect.NewRequest(&v1.ListTeamsRequest{}))167require.NoError(t, err)168requireEqualProto(t, &v1.ListTeamsResponse{169Teams: []*v1.Team{170teamToAPIResponse(teams[0], teamMembers, invite),171teamToAPIResponse(teams[1], teamMembers, invite),172},173}, response.Msg)174})175176t.Run("returns team with members and no invite for non-owner", func(t *testing.T) {177ctx := context.Background()178serverMock, client := setupTeamService(t)179180teamMembers := []*protocol.TeamMemberInfo{181newTeamMember(&protocol.TeamMemberInfo{182FullName: "Alice Alice",183Role: protocol.TeamMember_Owner,184}),185newTeamMember(&protocol.TeamMemberInfo{186FullName: "Bob Bob",187Role: protocol.TeamMember_Member,188}),189}190team := newTeam(&protocol.Team{191Name: "Team A",192})193serverMock.EXPECT().GetTeams(gomock.Any()).Return([]*protocol.Team{team}, nil)194195// Mock for populating team details196serverMock.EXPECT().GetTeamMembers(gomock.Any(), team.ID).Return(teamMembers, nil)197serverMock.EXPECT().GetGenericInvite(gomock.Any(), team.ID).Return(nil, &jsonrpc2.Error{Code: 403, Message: "not access"})198199response, err := client.ListTeams(ctx, connect.NewRequest(&v1.ListTeamsRequest{}))200require.NoError(t, err)201requireEqualProto(t, &v1.ListTeamsResponse{202Teams: []*v1.Team{203teamToAPIResponse(team, teamMembers, nil),204},205}, response.Msg)206})207}208209func TestTeamService_GetTeam(t *testing.T) {210var (211teamID = uuid.New().String()212)213214t.Run("returns invalid argument when empty ID provided", func(t *testing.T) {215_, client := setupTeamService(t)216217_, err := client.GetTeam(context.Background(), connect.NewRequest(&v1.GetTeamRequest{218TeamId: "",219}))220require.Error(t, err)221require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))222})223224t.Run("proxies request to server", func(t *testing.T) {225serverMock, client := setupTeamService(t)226227team := newTeam(&protocol.Team{228ID: teamID,229})230members := []*protocol.TeamMemberInfo{newTeamMember(&protocol.TeamMemberInfo{}), newTeamMember(&protocol.TeamMemberInfo{})}231invite := &protocol.TeamMembershipInvite{ID: uuid.New().String()}232233serverMock.EXPECT().GetTeam(gomock.Any(), teamID).Return(team, nil)234serverMock.EXPECT().GetTeamMembers(gomock.Any(), teamID).Return(members, nil)235serverMock.EXPECT().GetGenericInvite(gomock.Any(), teamID).Return(invite, nil)236237retrieved, err := client.GetTeam(context.Background(), connect.NewRequest(&v1.GetTeamRequest{238TeamId: teamID,239}))240require.NoError(t, err)241requireEqualProto(t, &v1.GetTeamResponse{242Team: teamToAPIResponse(team, members, invite),243}, retrieved.Msg)244})245}246247func TestTeamsService_JoinTeam(t *testing.T) {248249var (250teamID = uuid.New().String()251inviteID = uuid.New().String()252)253254t.Run("fails with invalid argument when no join ID is specified", func(t *testing.T) {255_, client := setupTeamService(t)256257_, err := client.JoinTeam(context.Background(), connect.NewRequest(&v1.JoinTeamRequest{258InvitationId: "",259}))260require.Error(t, err)261require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))262})263264t.Run("delegates joining to server, and populates team response", func(t *testing.T) {265serverMock, client := setupTeamService(t)266267team := newTeam(&protocol.Team{ID: teamID})268teamMembers := []*protocol.TeamMemberInfo{newTeamMember(&protocol.TeamMemberInfo{})}269invite := &protocol.TeamMembershipInvite{270ID: uuid.New().String(),271}272273serverMock.EXPECT().JoinTeam(gomock.Any(), inviteID).Return(team, nil)274serverMock.EXPECT().GetTeamMembers(gomock.Any(), teamID).Return(teamMembers, nil)275serverMock.EXPECT().GetGenericInvite(gomock.Any(), teamID).Return(invite, nil)276277response, err := client.JoinTeam(context.Background(), connect.NewRequest(&v1.JoinTeamRequest{278InvitationId: inviteID,279}))280require.NoError(t, err)281requireEqualProto(t, &v1.JoinTeamResponse{282Team: teamToAPIResponse(team, teamMembers, invite),283}, response.Msg)284})285}286287func TestTeamToAPIResponse(t *testing.T) {288// Here, we're deliberately not using our helpers newTeam, newTeamMembers because289// we want to assert from first principles290team := &protocol.Team{291ID: uuid.New().String(),292Name: "New Team",293CreationTime: "2022-09-09T09:09:09.000Z",294}295members := []*protocol.TeamMemberInfo{296{297UserId: uuid.New().String(),298FullName: "First Last",299PrimaryEmail: "[email protected]",300AvatarUrl: "https://avatars.com/foo",301Role: protocol.TeamMember_Member,302MemberSince: "2022-09-09T09:09:09.000Z",303},304{305UserId: uuid.New().String(),306FullName: "Second Last",307PrimaryEmail: "[email protected]",308AvatarUrl: "https://avatars.com/bar",309Role: protocol.TeamMember_Owner,310MemberSince: "2022-09-09T09:09:09.000Z",311},312}313invite := &protocol.TeamMembershipInvite{314ID: uuid.New().String(),315TeamID: uuid.New().String(),316Role: protocol.TeamMember_Member,317CreationTime: "2022-08-08T08:08:08.000Z",318InvalidationTime: "2022-11-11T11:11:11.000Z",319InvitedEmail: "[email protected]",320}321322response := teamToAPIResponse(team, members, invite)323requireEqualProto(t, &v1.Team{324Id: team.ID,325Name: team.Name,326Members: []*v1.TeamMember{327{328UserId: members[0].UserId,329Role: teamRoleToAPIResponse(members[0].Role),330MemberSince: parseGitpodTimeStampOrDefault(members[0].MemberSince),331AvatarUrl: members[0].AvatarUrl,332FullName: members[0].FullName,333PrimaryEmail: members[0].PrimaryEmail,334},335{336UserId: members[1].UserId,337Role: teamRoleToAPIResponse(members[1].Role),338MemberSince: parseGitpodTimeStampOrDefault(members[1].MemberSince),339AvatarUrl: members[1].AvatarUrl,340FullName: members[1].FullName,341PrimaryEmail: members[1].PrimaryEmail,342},343},344TeamInvitation: &v1.TeamInvitation{345Id: invite.ID,346},347}, response)348}349350func TestTeamsService_ListTeamMembers(t *testing.T) {351t.Run("missing team ID returns invalid argument", func(t *testing.T) {352_, client := setupTeamService(t)353354_, err := client.ListTeamMembers(context.Background(), connect.NewRequest(&v1.ListTeamMembersRequest{}))355require.Error(t, err)356require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))357})358359t.Run("returns permission denied for non-owner", func(t *testing.T) {360ctx := context.Background()361serverMock, client := setupTeamService(t)362363team := newTeam(&protocol.Team{364Name: "Team A",365})366serverMock.EXPECT().GetTeamMembers(gomock.Any(), team.ID).Return(nil, &jsonrpc2.Error{Code: 403, Message: "not access"})367368_, err := client.ListTeamMembers(ctx, connect.NewRequest(&v1.ListTeamMembersRequest{369TeamId: team.ID,370}))371require.Error(t, err)372require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))373})374375t.Run("returns members", func(t *testing.T) {376teamMembers := []*protocol.TeamMemberInfo{377newTeamMember(&protocol.TeamMemberInfo{378FullName: "Alice Alice",379Role: protocol.TeamMember_Owner,380}),381newTeamMember(&protocol.TeamMemberInfo{382FullName: "Bob Bob",383Role: protocol.TeamMember_Member,384}),385}386team := newTeam(&protocol.Team{387ID: uuid.New().String(),388})389390serverMock, client := setupTeamService(t)391392serverMock.EXPECT().GetTeamMembers(gomock.Any(), team.ID).Return(teamMembers, nil)393394response, err := client.ListTeamMembers(context.Background(), connect.NewRequest(&v1.ListTeamMembersRequest{TeamId: team.ID}))395require.NoError(t, err)396397requireEqualProto(t, &v1.ListTeamMembersResponse{398Members: teamMembersToAPIResponse(teamMembers),399}, response.Msg)400})401}402403func TestTeamsService_UpdateTeamMember(t *testing.T) {404var (405teamID = uuid.New().String()406teamMemberID = uuid.New().String()407)408409t.Run("invalid argument when team ID is missing", func(t *testing.T) {410_, client := setupTeamService(t)411412_, err := client.UpdateTeamMember(context.Background(), connect.NewRequest(&v1.UpdateTeamMemberRequest{}))413require.Error(t, err)414require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))415})416417t.Run("invalid argument when team member ID is missing", func(t *testing.T) {418_, client := setupTeamService(t)419420_, err := client.UpdateTeamMember(context.Background(), connect.NewRequest(&v1.UpdateTeamMemberRequest{421TeamId: teamID,422TeamMember: &v1.TeamMember{},423}))424require.Error(t, err)425require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))426})427428t.Run("invalid argument when team member role is missing", func(t *testing.T) {429_, client := setupTeamService(t)430431_, err := client.UpdateTeamMember(context.Background(), connect.NewRequest(&v1.UpdateTeamMemberRequest{432TeamId: teamID,433TeamMember: &v1.TeamMember{434UserId: teamMemberID,435},436}))437require.Error(t, err)438require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))439})440441t.Run("proxies request to server", func(t *testing.T) {442serverMock, client := setupTeamService(t)443444serverMock.EXPECT().SetTeamMemberRole(gomock.Any(), teamID, teamMemberID, protocol.TeamMember_Owner).Return(nil)445446response, err := client.UpdateTeamMember(context.Background(), connect.NewRequest(&v1.UpdateTeamMemberRequest{447TeamId: teamID,448TeamMember: &v1.TeamMember{449UserId: teamMemberID,450Role: v1.TeamRole_TEAM_ROLE_OWNER,451},452}))453require.NoError(t, err)454requireEqualProto(t, &v1.UpdateTeamMemberResponse{455TeamMember: &v1.TeamMember{456UserId: teamMemberID,457Role: v1.TeamRole_TEAM_ROLE_OWNER,458},459}, response.Msg)460})461}462463func TestTeamsService_DeleteTeamMember(t *testing.T) {464var (465teamID = uuid.New().String()466teamMemberID = uuid.New().String()467)468469t.Run("invalid argument when team ID is missing", func(t *testing.T) {470_, client := setupTeamService(t)471472_, err := client.DeleteTeamMember(context.Background(), connect.NewRequest(&v1.DeleteTeamMemberRequest{}))473require.Error(t, err)474require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))475})476477t.Run("invalid argument when team member ID is missing", func(t *testing.T) {478_, client := setupTeamService(t)479480_, err := client.DeleteTeamMember(context.Background(), connect.NewRequest(&v1.DeleteTeamMemberRequest{481TeamId: teamID,482}))483require.Error(t, err)484require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))485})486487t.Run("proxies to server", func(t *testing.T) {488serverMock, client := setupTeamService(t)489490serverMock.EXPECT().RemoveTeamMember(gomock.Any(), teamID, teamMemberID).Return(nil)491492response, err := client.DeleteTeamMember(context.Background(), connect.NewRequest(&v1.DeleteTeamMemberRequest{493TeamId: teamID,494TeamMemberId: teamMemberID,495}))496require.NoError(t, err)497requireEqualProto(t, &v1.DeleteTeamMemberResponse{}, response.Msg)498})499}500501func TestTeamService_ResetTeamInvitation(t *testing.T) {502t.Run("missing team ID returns invalid argument", func(t *testing.T) {503_, client := setupTeamService(t)504505_, err := client.ResetTeamInvitation(context.Background(), connect.NewRequest(&v1.ResetTeamInvitationRequest{}))506require.Error(t, err)507require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))508})509510t.Run("proxies request to server", func(t *testing.T) {511teamID := uuid.New().String()512513serverMock, client := setupTeamService(t)514515invite := &protocol.TeamMembershipInvite{516ID: uuid.New().String(),517}518519serverMock.EXPECT().ResetGenericInvite(gomock.Any(), teamID).Return(invite, nil)520521response, err := client.ResetTeamInvitation(context.Background(), connect.NewRequest(&v1.ResetTeamInvitationRequest{522TeamId: teamID,523}))524require.NoError(t, err)525requireEqualProto(t, &v1.ResetTeamInvitationResponse{526TeamInvitation: teamInviteToAPIResponse(invite),527}, response.Msg)528})529}530531func TestTeamService_GetTeamInvitation(t *testing.T) {532t.Run("missing team ID returns invalid argument", func(t *testing.T) {533_, client := setupTeamService(t)534535_, err := client.GetTeamInvitation(context.Background(), connect.NewRequest(&v1.GetTeamInvitationRequest{}))536require.Error(t, err)537require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))538})539540t.Run("proxies request to server", func(t *testing.T) {541teamID := uuid.New().String()542543serverMock, client := setupTeamService(t)544545invite := &protocol.TeamMembershipInvite{546ID: uuid.New().String(),547}548549serverMock.EXPECT().GetGenericInvite(gomock.Any(), teamID).Return(invite, nil)550551response, err := client.GetTeamInvitation(context.Background(), connect.NewRequest(&v1.GetTeamInvitationRequest{552TeamId: teamID,553}))554require.NoError(t, err)555requireEqualProto(t, &v1.GetTeamInvitationResponse{556TeamInvitation: teamInviteToAPIResponse(invite),557}, response.Msg)558})559560t.Run("returns permission denied for non-owner", func(t *testing.T) {561ctx := context.Background()562serverMock, client := setupTeamService(t)563564team := newTeam(&protocol.Team{565Name: "Team A",566})567serverMock.EXPECT().GetGenericInvite(gomock.Any(), team.ID).Return(nil, &jsonrpc2.Error{Code: 403, Message: "not access"})568569_, err := client.GetTeamInvitation(ctx, connect.NewRequest(&v1.GetTeamInvitationRequest{570TeamId: team.ID,571}))572require.Error(t, err)573require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err))574})575}576577func TestTeamService_DeleteTeam(t *testing.T) {578t.Run("missing team ID returns invalid argument", func(t *testing.T) {579_, client := setupTeamService(t)580581_, err := client.DeleteTeam(context.Background(), connect.NewRequest(&v1.DeleteTeamRequest{}))582require.Error(t, err)583require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))584})585586t.Run("proxies request to server", func(t *testing.T) {587teamID := uuid.New().String()588589serverMock, client := setupTeamService(t)590591serverMock.EXPECT().DeleteTeam(gomock.Any(), teamID).Return(nil)592593response, err := client.DeleteTeam(context.Background(), connect.NewRequest(&v1.DeleteTeamRequest{594TeamId: teamID,595}))596require.NoError(t, err)597requireEqualProto(t, &v1.DeleteTeamResponse{}, response.Msg)598})599}600601func newTeam(t *protocol.Team) *protocol.Team {602result := &protocol.Team{603ID: uuid.New().String(),604Name: "Team Name",605CreationTime: "2022-10-10T10:10:10.000Z",606}607608if t.ID != "" {609result.ID = t.ID610}611612if t.Name != "" {613result.Name = t.Name614}615616if t.CreationTime != "" {617result.CreationTime = t.CreationTime618}619620return result621}622623func newTeamMember(m *protocol.TeamMemberInfo) *protocol.TeamMemberInfo {624result := &protocol.TeamMemberInfo{625UserId: uuid.New().String(),626FullName: "First Last",627PrimaryEmail: "[email protected]",628AvatarUrl: "https://avatars.yolo/first.png",629Role: protocol.TeamMember_Member,630MemberSince: "2022-09-09T09:09:09.000Z",631}632633if m.UserId != "" {634result.UserId = m.UserId635}636if m.FullName != "" {637result.FullName = m.FullName638}639if m.PrimaryEmail != "" {640result.PrimaryEmail = m.PrimaryEmail641}642if m.AvatarUrl != "" {643result.AvatarUrl = m.AvatarUrl644}645if m.Role != "" {646result.Role = m.Role647}648if m.MemberSince != "" {649result.MemberSince = m.MemberSince650}651652return result653}654655func setupTeamService(t *testing.T) (*protocol.MockAPIInterface, v1connect.TeamsServiceClient) {656t.Helper()657658ctrl := gomock.NewController(t)659t.Cleanup(ctrl.Finish)660661serverMock := protocol.NewMockAPIInterface(ctrl)662663svc := NewTeamsService(&FakeServerConnPool{664api: serverMock,665})666667keyset := jwstest.GenerateKeySet(t)668rsa256, err := jws.NewRSA256(keyset)669require.NoError(t, err)670671_, handler := v1connect.NewTeamsServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor(config.SessionConfig{672Issuer: "unitetest.com",673Cookie: config.CookieConfig{674Name: "cookie_jwt",675},676}, rsa256)))677678srv := httptest.NewServer(handler)679t.Cleanup(srv.Close)680681client := v1connect.NewTeamsServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(682auth.NewClientInterceptor("auth-token"),683))684685return serverMock, client686}687688func requireEqualProto(t *testing.T, expected interface{}, actual interface{}) {689t.Helper()690691diff := cmp.Diff(expected, actual, protocmp.Transform())692if diff != "" {693require.Fail(t, diff)694}695}696697698