Skip to content

Commit 56abf4d

Browse files
committed
[public-api] Implement experimental TeamsService.CreateTeam
1 parent 92dd887 commit 56abf4d

File tree

7 files changed

+263
-0
lines changed

7 files changed

+263
-0
lines changed

components/public-api-server/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/gitpod-io/gitpod/usage-api v0.0.0-00010101000000-000000000000
1212
github.com/golang/mock v1.6.0
1313
github.com/google/go-cmp v0.5.9
14+
github.com/google/uuid v1.1.2
1415
github.com/gorilla/handlers v1.5.1
1516
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
1617
github.com/prometheus/client_golang v1.13.0

components/public-api-server/go.sum

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package apiv1
6+
7+
import (
8+
"context"
9+
"fmt"
10+
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
11+
12+
connect "github.com/bufbuild/connect-go"
13+
"github.com/gitpod-io/gitpod/common-go/log"
14+
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
15+
"github.com/gitpod-io/gitpod/public-api-server/pkg/proxy"
16+
v1 "github.com/gitpod-io/gitpod/public-api/experimental/v1"
17+
"github.com/gitpod-io/gitpod/public-api/experimental/v1/v1connect"
18+
)
19+
20+
func NewTeamsService(pool proxy.ServerConnectionPool) *TeamService {
21+
return &TeamService{
22+
connectionPool: pool,
23+
}
24+
}
25+
26+
var _ v1connect.TeamsServiceHandler = (*TeamService)(nil)
27+
28+
type TeamService struct {
29+
connectionPool proxy.ServerConnectionPool
30+
31+
v1connect.UnimplementedTeamsServiceHandler
32+
}
33+
34+
func (s *TeamService) CreateTeam(ctx context.Context, req *connect.Request[v1.CreateTeamRequest]) (*connect.Response[v1.CreateTeamResponse], error) {
35+
token := auth.TokenFromContext(ctx)
36+
37+
if req.Msg.GetName() == "" {
38+
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Name is a required argument when creating a team."))
39+
}
40+
41+
server, err := s.connectionPool.Get(ctx, token)
42+
if err != nil {
43+
log.Log.WithError(err).Error("Failed to get connection to server.")
44+
return nil, connect.NewError(connect.CodeInternal, err)
45+
}
46+
47+
team, err := server.CreateTeam(ctx, req.Msg.GetName())
48+
if err != nil {
49+
return nil, proxy.ConvertError(err)
50+
}
51+
52+
members, err := server.GetTeamMembers(ctx, team.ID)
53+
if err != nil {
54+
return nil, proxy.ConvertError(err)
55+
}
56+
57+
invite, err := server.GetGenericInvite(ctx, team.ID)
58+
if err != nil {
59+
return nil, proxy.ConvertError(err)
60+
}
61+
62+
return connect.NewResponse(&v1.CreateTeamResponse{
63+
Team: &v1.Team{
64+
Id: team.ID,
65+
Name: team.Name,
66+
Slug: team.Slug,
67+
Members: teamMembersToAPIResponse(members),
68+
TeamInvitation: &v1.TeamInvitation{
69+
Id: invite.ID,
70+
},
71+
},
72+
}), nil
73+
}
74+
75+
func teamMembersToAPIResponse(members []*protocol.TeamMemberInfo) []*v1.TeamMember {
76+
var result []*v1.TeamMember
77+
78+
for _, m := range members {
79+
result = append(result, &v1.TeamMember{
80+
UserId: m.UserId,
81+
Role: teamRoleToAPIResponse(m.Role),
82+
})
83+
}
84+
85+
return result
86+
}
87+
88+
func teamRoleToAPIResponse(role protocol.TeamMemberRole) v1.TeamRole {
89+
switch role {
90+
case protocol.TeamMember_Owner:
91+
return v1.TeamRole_TEAM_ROLE_OWNER
92+
case protocol.TeamMember_Member:
93+
return v1.TeamRole_TEAM_ROLE_MEMBER
94+
default:
95+
return v1.TeamRole_TEAM_ROLE_UNSPECIFIED
96+
}
97+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package apiv1
6+
7+
import (
8+
"context"
9+
"errors"
10+
"github.com/bufbuild/connect-go"
11+
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
12+
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
13+
v1 "github.com/gitpod-io/gitpod/public-api/experimental/v1"
14+
"github.com/gitpod-io/gitpod/public-api/experimental/v1/v1connect"
15+
"github.com/golang/mock/gomock"
16+
"github.com/google/go-cmp/cmp"
17+
"github.com/google/uuid"
18+
"github.com/stretchr/testify/require"
19+
"google.golang.org/protobuf/testing/protocmp"
20+
"net/http"
21+
"net/http/httptest"
22+
"testing"
23+
"time"
24+
)
25+
26+
func TestTeamsService_CreateTeam(t *testing.T) {
27+
28+
var (
29+
name = "Shiny New Team"
30+
slug = "shiny-new-team"
31+
id = uuid.New().String()
32+
)
33+
34+
t.Run("returns invalid request when server returns invalid request", func(t *testing.T) {
35+
ctx := context.Background()
36+
_, serverMock, client := setupTeamService(t)
37+
38+
serverMock.EXPECT().CreateTeam(gomock.Any(), name).Return(nil, errors.New("code 400"))
39+
40+
_, err := client.CreateTeam(ctx, connect.NewRequest(&v1.CreateTeamRequest{Name: name}))
41+
require.Error(t, err)
42+
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
43+
})
44+
45+
t.Run("returns team with members and invite", func(t *testing.T) {
46+
ts := time.Now().UTC()
47+
teamMembers := []*protocol.TeamMemberInfo{
48+
{
49+
UserId: uuid.New().String(),
50+
FullName: "Alice Alice",
51+
PrimaryEmail: "[email protected]",
52+
AvatarUrl: "",
53+
Role: protocol.TeamMember_Owner,
54+
MemberSince: "",
55+
}, {
56+
UserId: uuid.New().String(),
57+
FullName: "Bob Bob",
58+
PrimaryEmail: "[email protected]",
59+
AvatarUrl: "",
60+
Role: protocol.TeamMember_Member,
61+
MemberSince: "",
62+
},
63+
}
64+
inviteID := uuid.New().String()
65+
66+
_, serverMock, client := setupTeamService(t)
67+
68+
serverMock.EXPECT().CreateTeam(gomock.Any(), name).Return(&protocol.Team{
69+
ID: id,
70+
Name: name,
71+
Slug: slug,
72+
CreationTime: ts.String(),
73+
}, nil)
74+
serverMock.EXPECT().GetTeamMembers(gomock.Any(), id).Return(teamMembers, nil)
75+
serverMock.EXPECT().GetGenericInvite(gomock.Any(), id).Return(&protocol.TeamMembershipInvite{
76+
ID: inviteID,
77+
TeamID: id,
78+
Role: "",
79+
CreationTime: "",
80+
InvalidationTime: "",
81+
InvitedEmail: "",
82+
}, nil)
83+
84+
response, err := client.CreateTeam(context.Background(), connect.NewRequest(&v1.CreateTeamRequest{Name: name}))
85+
require.NoError(t, err)
86+
87+
requireEqualProto(t, &v1.CreateTeamResponse{
88+
Team: &v1.Team{
89+
Id: id,
90+
Name: name,
91+
Slug: slug,
92+
Members: []*v1.TeamMember{
93+
{
94+
UserId: teamMembers[0].UserId,
95+
Role: teamRoleToAPIResponse(teamMembers[0].Role),
96+
},
97+
{
98+
UserId: teamMembers[1].UserId,
99+
Role: teamRoleToAPIResponse(teamMembers[1].Role),
100+
},
101+
},
102+
TeamInvitation: &v1.TeamInvitation{
103+
Id: inviteID,
104+
},
105+
},
106+
}, response.Msg)
107+
})
108+
}
109+
110+
func setupTeamService(t *testing.T) (*TeamService, *protocol.MockAPIInterface, v1connect.TeamsServiceClient) {
111+
t.Helper()
112+
113+
ctrl := gomock.NewController(t)
114+
t.Cleanup(ctrl.Finish)
115+
116+
serverMock := protocol.NewMockAPIInterface(ctrl)
117+
118+
svc := NewTeamsService(&FakeServerConnPool{
119+
api: serverMock,
120+
})
121+
122+
_, handler := v1connect.NewTeamsServiceHandler(svc)
123+
124+
srv := httptest.NewServer(handler)
125+
t.Cleanup(srv.Close)
126+
127+
client := v1connect.NewTeamsServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(
128+
auth.NewClientInterceptor("auth-token"),
129+
))
130+
131+
return svc, serverMock, client
132+
}
133+
134+
func requireEqualProto(t *testing.T, expected interface{}, actual interface{}) {
135+
t.Helper()
136+
137+
diff := cmp.Diff(expected, actual, protocmp.Transform())
138+
if diff != "" {
139+
require.Fail(t, diff)
140+
}
141+
}

components/public-api-server/pkg/proxy/errors.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ func ConvertError(err error) error {
1717

1818
s := err.Error()
1919

20+
// components/gitpod-protocol/src/messaging/error.ts
21+
if strings.Contains(s, "code 400") {
22+
return connect.NewError(connect.CodeInvalidArgument, err)
23+
}
24+
25+
// components/gitpod-protocol/src/messaging/error.ts
2026
if strings.Contains(s, "code 401") {
2127
return connect.NewError(connect.CodePermissionDenied, err)
2228
}
@@ -31,6 +37,11 @@ func ConvertError(err error) error {
3137
return connect.NewError(connect.CodeNotFound, err)
3238
}
3339

40+
// components/gitpod-protocol/src/messaging/error.ts
41+
if strings.Contains(s, "code 409") {
42+
return connect.NewError(connect.CodeAlreadyExists, err)
43+
}
44+
3445
// components/gitpod-messagebus/src/jsonrpc-server.ts#47
3546
if strings.Contains(s, "code -32603") {
3647
return connect.NewError(connect.CodeInternal, err)

components/public-api-server/pkg/proxy/errors_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ func TestConvertError(t *testing.T) {
2626
WebsocketError: errors.New("jsonrpc2: code -32603 message: Request getWorkspace failed with message: No workspace with id 'some-id' found."),
2727
ExpectedStatus: connect.CodeInternal,
2828
},
29+
{
30+
WebsocketError: errors.New("code 400"),
31+
ExpectedStatus: connect.CodeInvalidArgument,
32+
},
33+
{
34+
WebsocketError: errors.New("code 409"),
35+
ExpectedStatus: connect.CodeAlreadyExists,
36+
},
2937
}
3038

3139
for _, s := range scenarios {

components/public-api-server/pkg/server/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ func register(srv *baseserver.Server, connPool proxy.ServerConnectionPool) error
9999
workspacesRoute, workspacesServiceHandler := v1connect.NewWorkspacesServiceHandler(apiv1.NewWorkspaceService(connPool), handlerOptions...)
100100
srv.HTTPMux().Handle(workspacesRoute, workspacesServiceHandler)
101101

102+
teamsRoute, teamsServiceHandler := v1connect.NewTeamsServiceHandler(apiv1.NewTeamsService(connPool), handlerOptions...)
103+
srv.HTTPMux().Handle(teamsRoute, teamsServiceHandler)
104+
102105
return nil
103106
}
104107

0 commit comments

Comments
 (0)