From b5708a19d60bf580201c44c7306f2bdaf7b5dcea Mon Sep 17 00:00:00 2001 From: Milan Pavlik Date: Sun, 30 Oct 2022 20:20:38 +0000 Subject: [PATCH] [public-api] Implement experimental ListTeams --- .../public-api-server/pkg/apiv1/team.go | 79 ++++++++++--- .../public-api-server/pkg/apiv1/team_test.go | 110 +++++++++++++++++- 2 files changed, 167 insertions(+), 22 deletions(-) diff --git a/components/public-api-server/pkg/apiv1/team.go b/components/public-api-server/pkg/apiv1/team.go index 273295eb40d151..f9fc059babdb20 100644 --- a/components/public-api-server/pkg/apiv1/team.go +++ b/components/public-api-server/pkg/apiv1/team.go @@ -8,16 +8,15 @@ import ( "context" "fmt" - protocol "github.com/gitpod-io/gitpod/gitpod-protocol" - "github.com/relvacode/iso8601" - "google.golang.org/protobuf/types/known/timestamppb" - connect "github.com/bufbuild/connect-go" "github.com/gitpod-io/gitpod/common-go/log" + protocol "github.com/gitpod-io/gitpod/gitpod-protocol" "github.com/gitpod-io/gitpod/public-api-server/pkg/auth" "github.com/gitpod-io/gitpod/public-api-server/pkg/proxy" v1 "github.com/gitpod-io/gitpod/public-api/experimental/v1" "github.com/gitpod-io/gitpod/public-api/experimental/v1/v1connect" + "github.com/relvacode/iso8601" + "google.golang.org/protobuf/types/known/timestamppb" ) func NewTeamsService(pool proxy.ServerConnectionPool) *TeamService { @@ -41,38 +40,82 @@ func (s *TeamService) CreateTeam(ctx context.Context, req *connect.Request[v1.Cr return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Name is a required argument when creating a team.")) } - server, err := s.connectionPool.Get(ctx, token) + conn, err := s.connectionPool.Get(ctx, token) if err != nil { log.Log.WithError(err).Error("Failed to get connection to server.") return nil, connect.NewError(connect.CodeInternal, err) } - team, err := server.CreateTeam(ctx, req.Msg.GetName()) + created, err := conn.CreateTeam(ctx, req.Msg.GetName()) if err != nil { + log.WithError(err).Error("Failed to create team.") return nil, proxy.ConvertError(err) } - members, err := server.GetTeamMembers(ctx, team.ID) + team, err := s.toTeamAPIResponse(ctx, conn, created) if err != nil { + log.WithError(err).Error("Failed to populate team with details.") + return nil, err + } + + return connect.NewResponse(&v1.CreateTeamResponse{ + Team: team, + }), nil +} + +func (s *TeamService) ListTeams(ctx context.Context, req *connect.Request[v1.ListTeamsRequest]) (*connect.Response[v1.ListTeamsResponse], error) { + token := auth.TokenFromContext(ctx) + + conn, err := s.connectionPool.Get(ctx, token) + if err != nil { + log.Log.WithError(err).Error("Failed to get connection to server.") + return nil, connect.NewError(connect.CodeInternal, err) + } + + teams, err := conn.GetTeams(ctx) + if err != nil { + log.WithError(err).Error("Failed to list teams from server.") + return nil, proxy.ConvertError(err) + } + + var response []*v1.Team + for _, t := range teams { + team, err := s.toTeamAPIResponse(ctx, conn, t) + if err != nil { + log.WithError(err).Error("Failed to populate team with details.") + return nil, err + } + + response = append(response, team) + } + + return connect.NewResponse(&v1.ListTeamsResponse{ + Teams: response, + }), nil +} + +func (s *TeamService) toTeamAPIResponse(ctx context.Context, conn protocol.APIInterface, team *protocol.Team) (*v1.Team, error) { + members, err := conn.GetTeamMembers(ctx, team.ID) + if err != nil { + log.WithError(err).Error("Failed to get team members.") return nil, proxy.ConvertError(err) } - invite, err := server.GetGenericInvite(ctx, team.ID) + invite, err := conn.GetGenericInvite(ctx, team.ID) if err != nil { + log.WithError(err).Error("Failed to get generic invite.") return nil, proxy.ConvertError(err) } - return connect.NewResponse(&v1.CreateTeamResponse{ - Team: &v1.Team{ - Id: team.ID, - Name: team.Name, - Slug: team.Slug, - Members: teamMembersToAPIResponse(members), - TeamInvitation: &v1.TeamInvitation{ - Id: invite.ID, - }, + return &v1.Team{ + Id: team.ID, + Name: team.Name, + Slug: team.Slug, + Members: teamMembersToAPIResponse(members), + TeamInvitation: &v1.TeamInvitation{ + Id: invite.ID, }, - }), nil + }, nil } func teamMembersToAPIResponse(members []*protocol.TeamMemberInfo) []*v1.TeamMember { diff --git a/components/public-api-server/pkg/apiv1/team_test.go b/components/public-api-server/pkg/apiv1/team_test.go index 8a513d5e95db91..4e8b3a6fddc4a4 100644 --- a/components/public-api-server/pkg/apiv1/team_test.go +++ b/components/public-api-server/pkg/apiv1/team_test.go @@ -25,13 +25,16 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" ) +const ( + memberSince = "2022-10-10T10:10:10.000Z" +) + func TestTeamsService_CreateTeam(t *testing.T) { var ( - name = "Shiny New Team" - slug = "shiny-new-team" - id = uuid.New().String() - memberSince = "2022-10-10T10:10:10.000Z" + name = "Shiny New Team" + slug = "shiny-new-team" + id = uuid.New().String() ) t.Run("returns invalid argument when name is empty", func(t *testing.T) { @@ -121,6 +124,105 @@ func TestTeamsService_CreateTeam(t *testing.T) { }) } +func TestTeamsService_ListTeams(t *testing.T) { + t.Run("returns teams with members and invite", func(t *testing.T) { + ctx := context.Background() + serverMock, client := setupTeamService(t) + + teamMembers := []*protocol.TeamMemberInfo{ + { + UserId: uuid.New().String(), + FullName: "Alice Alice", + PrimaryEmail: "alice@alice.com", + AvatarUrl: "", + Role: protocol.TeamMember_Owner, + MemberSince: memberSince, + }, { + UserId: uuid.New().String(), + FullName: "Bob Bob", + PrimaryEmail: "bob@bob.com", + AvatarUrl: "", + Role: protocol.TeamMember_Member, + MemberSince: memberSince, + }, + } + teams := []*protocol.Team{ + { + ID: uuid.New().String(), + Name: "Team A", + Slug: "team_a", + }, { + ID: uuid.New().String(), + Name: "Team B", + Slug: "team_ba", + }, + } + inviteID := uuid.New().String() + + serverMock.EXPECT().GetTeams(gomock.Any()).Return(teams, nil) + + // Mocks for populating team A details + serverMock.EXPECT().GetTeamMembers(gomock.Any(), teams[0].ID).Return(teamMembers, nil) + serverMock.EXPECT().GetGenericInvite(gomock.Any(), teams[0].ID).Return(&protocol.TeamMembershipInvite{ + ID: inviteID, + TeamID: teams[0].ID, + }, nil) + // Mock for populating team B details + serverMock.EXPECT().GetTeamMembers(gomock.Any(), teams[1].ID).Return(teamMembers, nil) + serverMock.EXPECT().GetGenericInvite(gomock.Any(), teams[1].ID).Return(&protocol.TeamMembershipInvite{ + ID: inviteID, + TeamID: teams[1].ID, + }, nil) + + response, err := client.ListTeams(ctx, connect.NewRequest(&v1.ListTeamsRequest{})) + require.NoError(t, err) + requireEqualProto(t, &v1.ListTeamsResponse{ + Teams: []*v1.Team{ + { + Id: teams[0].ID, + Name: teams[0].Name, + Slug: teams[0].Slug, + Members: []*v1.TeamMember{ + { + UserId: teamMembers[0].UserId, + Role: teamRoleToAPIResponse(teamMembers[0].Role), + MemberSince: parseTimeStamp(memberSince), + }, + { + UserId: teamMembers[1].UserId, + Role: teamRoleToAPIResponse(teamMembers[1].Role), + MemberSince: parseTimeStamp(memberSince), + }, + }, + TeamInvitation: &v1.TeamInvitation{ + Id: inviteID, + }, + }, + { + Id: teams[1].ID, + Name: teams[1].Name, + Slug: teams[1].Slug, + Members: []*v1.TeamMember{ + { + UserId: teamMembers[0].UserId, + Role: teamRoleToAPIResponse(teamMembers[0].Role), + MemberSince: parseTimeStamp(memberSince), + }, + { + UserId: teamMembers[1].UserId, + Role: teamRoleToAPIResponse(teamMembers[1].Role), + MemberSince: parseTimeStamp(memberSince), + }, + }, + TeamInvitation: &v1.TeamInvitation{ + Id: inviteID, + }, + }, + }, + }, response.Msg) + }) +} + func setupTeamService(t *testing.T) (*protocol.MockAPIInterface, v1connect.TeamsServiceClient) { t.Helper()