Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/public-api-server/pkg/apiv1/project_test.go
2499 views
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
"math/rand"
11
"net/http"
12
"net/http/httptest"
13
"testing"
14
15
connect "github.com/bufbuild/connect-go"
16
"github.com/gitpod-io/gitpod/components/public-api/go/config"
17
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
18
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
19
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
20
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
21
"github.com/gitpod-io/gitpod/public-api-server/pkg/jws"
22
"github.com/gitpod-io/gitpod/public-api-server/pkg/jws/jwstest"
23
"github.com/golang/mock/gomock"
24
"github.com/google/uuid"
25
"github.com/stretchr/testify/require"
26
)
27
28
func TestProjectsService_CreateProject(t *testing.T) {
29
30
t.Run("returns invalid argument when request validation fails", func(t *testing.T) {
31
for _, s := range []struct {
32
Name string
33
Spec *v1.Project
34
ExpectedError string
35
}{
36
{
37
Name: "name is required",
38
Spec: &v1.Project{},
39
ExpectedError: "Name is a required argument.",
40
},
41
{
42
Name: "whitespace name is rejected",
43
Spec: &v1.Project{
44
Name: " ",
45
},
46
ExpectedError: "Name is a required argument.",
47
},
48
{
49
Name: "clone url is required",
50
Spec: &v1.Project{
51
Name: "name",
52
},
53
ExpectedError: "Clone URL is a required argument.",
54
},
55
{
56
Name: "whitespace clone url is rejected",
57
Spec: &v1.Project{
58
Name: "name",
59
CloneUrl: " ",
60
},
61
ExpectedError: "Clone URL is a required argument.",
62
},
63
{
64
Name: "team ID must be a valid UUID",
65
Spec: &v1.Project{
66
Name: "name",
67
CloneUrl: "some.clone.url",
68
TeamId: "my-user",
69
},
70
ExpectedError: "Team ID is not a valid UUID.",
71
},
72
} {
73
t.Run(s.Name, func(t *testing.T) {
74
_, client := setupProjectsService(t)
75
76
_, err := client.CreateProject(context.Background(), connect.NewRequest(&v1.CreateProjectRequest{
77
Project: s.Spec,
78
}))
79
require.Error(t, err)
80
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
81
require.Equal(t, s.ExpectedError, err.(*connect.Error).Message())
82
})
83
}
84
})
85
86
t.Run("proxies request to server", func(t *testing.T) {
87
projectsMock, client := setupProjectsService(t)
88
89
project := newProject(&protocol.Project{
90
Settings: &protocol.ProjectSettings{},
91
})
92
93
projectsMock.EXPECT().CreateProject(gomock.Any(), &protocol.CreateProjectOptions{
94
TeamID: project.TeamID,
95
Name: project.Name,
96
CloneURL: project.CloneURL,
97
AppInstallationID: "undefined",
98
}).Return(project, nil)
99
100
response, err := client.CreateProject(context.Background(), connect.NewRequest(&v1.CreateProjectRequest{
101
Project: &v1.Project{
102
Name: project.Name,
103
CloneUrl: project.CloneURL,
104
TeamId: project.TeamID,
105
},
106
}))
107
require.NoError(t, err)
108
requireEqualProto(t, &v1.CreateProjectResponse{
109
Project: projectToAPIResponse(project),
110
}, response.Msg)
111
112
})
113
}
114
115
func TestProjectsService_ListProjects(t *testing.T) {
116
117
t.Run("invalid argument when org id is missing", func(t *testing.T) {
118
_, client := setupProjectsService(t)
119
120
_, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{}))
121
require.Error(t, err)
122
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
123
})
124
125
t.Run("invalid argument when org ID is not a valid UUID", func(t *testing.T) {
126
_, client := setupProjectsService(t)
127
128
_, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
129
TeamId: "some-id",
130
}))
131
require.Error(t, err)
132
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
133
})
134
135
t.Run("no projects from server return empty list", func(t *testing.T) {
136
serverMock, client := setupProjectsService(t)
137
138
teamID := uuid.New().String()
139
serverMock.EXPECT().GetTeamProjects(gomock.Any(), teamID).Return(nil, nil)
140
141
response, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
142
TeamId: teamID,
143
}))
144
require.NoError(t, err)
145
requireEqualProto(t, &v1.ListProjectsResponse{
146
Projects: nil,
147
TotalResults: 0,
148
}, response.Msg)
149
})
150
151
t.Run("retrieves projects for team and paginates", func(t *testing.T) {
152
serverMock, client := setupProjectsService(t)
153
154
projects := []*protocol.Project{
155
newProject(&protocol.Project{}),
156
newProject(&protocol.Project{}),
157
newProject(&protocol.Project{}),
158
newProject(&protocol.Project{}),
159
newProject(&protocol.Project{}),
160
}
161
162
teamID := uuid.New().String()
163
serverMock.EXPECT().GetTeamProjects(gomock.Any(), teamID).Return(projects, nil).Times(3)
164
165
firstPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
166
TeamId: teamID,
167
Pagination: &v1.Pagination{
168
PageSize: 2,
169
},
170
}))
171
require.NoError(t, err)
172
requireEqualProto(t, &v1.ListProjectsResponse{
173
Projects: projectsToAPIResponse(projects[0:2]),
174
TotalResults: int32(len(projects)),
175
}, firstPage.Msg)
176
177
secondPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
178
TeamId: teamID,
179
Pagination: &v1.Pagination{
180
PageSize: 2,
181
Page: 2,
182
},
183
}))
184
require.NoError(t, err)
185
requireEqualProto(t, &v1.ListProjectsResponse{
186
Projects: projectsToAPIResponse(projects[2:4]),
187
TotalResults: int32(len(projects)),
188
}, secondPage.Msg)
189
190
thirdPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
191
TeamId: teamID,
192
Pagination: &v1.Pagination{
193
PageSize: 2,
194
Page: 3,
195
},
196
}))
197
require.NoError(t, err)
198
requireEqualProto(t, &v1.ListProjectsResponse{
199
Projects: projectsToAPIResponse(projects[4:]),
200
TotalResults: int32(len(projects)),
201
}, thirdPage.Msg)
202
})
203
204
t.Run("retrieves projects for team, when team ID is specified and paginates", func(t *testing.T) {
205
serverMock, client := setupProjectsService(t)
206
207
teamID := uuid.New().String()
208
209
projects := []*protocol.Project{
210
newProject(&protocol.Project{}),
211
newProject(&protocol.Project{}),
212
newProject(&protocol.Project{}),
213
newProject(&protocol.Project{}),
214
newProject(&protocol.Project{}),
215
}
216
217
serverMock.EXPECT().GetTeamProjects(gomock.Any(), teamID).Return(projects, nil).Times(3)
218
219
firstPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
220
TeamId: teamID,
221
Pagination: &v1.Pagination{
222
PageSize: 2,
223
},
224
}))
225
require.NoError(t, err)
226
requireEqualProto(t, &v1.ListProjectsResponse{
227
Projects: projectsToAPIResponse(projects[0:2]),
228
TotalResults: int32(len(projects)),
229
}, firstPage.Msg)
230
231
secondPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
232
TeamId: teamID,
233
Pagination: &v1.Pagination{
234
PageSize: 2,
235
Page: 2,
236
},
237
}))
238
require.NoError(t, err)
239
requireEqualProto(t, &v1.ListProjectsResponse{
240
Projects: projectsToAPIResponse(projects[2:4]),
241
TotalResults: int32(len(projects)),
242
}, secondPage.Msg)
243
244
thirdPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{
245
TeamId: teamID,
246
Pagination: &v1.Pagination{
247
PageSize: 2,
248
Page: 3,
249
},
250
}))
251
require.NoError(t, err)
252
requireEqualProto(t, &v1.ListProjectsResponse{
253
Projects: projectsToAPIResponse(projects[4:]),
254
TotalResults: int32(len(projects)),
255
}, thirdPage.Msg)
256
})
257
}
258
259
func TestProjectsService_DeleteProject(t *testing.T) {
260
261
t.Run("invalid argument when project ID is empty", func(t *testing.T) {
262
_, client := setupProjectsService(t)
263
264
_, err := client.DeleteProject(context.Background(), connect.NewRequest(&v1.DeleteProjectRequest{
265
ProjectId: "",
266
}))
267
require.Error(t, err)
268
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
269
})
270
271
t.Run("invalid argument when project ID is not a valid uuid", func(t *testing.T) {
272
_, client := setupProjectsService(t)
273
274
_, err := client.DeleteProject(context.Background(), connect.NewRequest(&v1.DeleteProjectRequest{
275
ProjectId: "something",
276
}))
277
require.Error(t, err)
278
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
279
})
280
281
t.Run("proxies to server", func(t *testing.T) {
282
serverMock, client := setupProjectsService(t)
283
284
projectID := uuid.New().String()
285
286
serverMock.EXPECT().DeleteProject(gomock.Any(), projectID).Return(nil)
287
288
resp, err := client.DeleteProject(context.Background(), connect.NewRequest(&v1.DeleteProjectRequest{
289
ProjectId: projectID,
290
}))
291
require.NoError(t, err)
292
requireEqualProto(t, &v1.DeleteProjectResponse{}, resp.Msg)
293
})
294
}
295
296
func setupProjectsService(t *testing.T) (*protocol.MockAPIInterface, v1connect.ProjectsServiceClient) {
297
t.Helper()
298
299
ctrl := gomock.NewController(t)
300
t.Cleanup(ctrl.Finish)
301
302
serverMock := protocol.NewMockAPIInterface(ctrl)
303
304
svc := NewProjectsService(&FakeServerConnPool{
305
api: serverMock,
306
})
307
308
keyset := jwstest.GenerateKeySet(t)
309
rsa256, err := jws.NewRSA256(keyset)
310
require.NoError(t, err)
311
312
_, handler := v1connect.NewProjectsServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor(config.SessionConfig{
313
Issuer: "unitetest.com",
314
Cookie: config.CookieConfig{
315
Name: "cookie_jwt",
316
},
317
}, rsa256)))
318
319
srv := httptest.NewServer(handler)
320
t.Cleanup(srv.Close)
321
322
client := v1connect.NewProjectsServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(
323
auth.NewClientInterceptor("auth-token"),
324
))
325
326
return serverMock, client
327
}
328
329
func newProject(p *protocol.Project) *protocol.Project {
330
r := rand.Int()
331
b_false := false
332
result := &protocol.Project{
333
ID: uuid.New().String(),
334
Name: fmt.Sprintf("team-%d", r),
335
TeamID: uuid.New().String(),
336
CloneURL: "https://github.com/easyCZ/foobar",
337
AppInstallationID: "1337",
338
Settings: &protocol.ProjectSettings{
339
UsePersistentVolumeClaim: true,
340
WorkspaceClasses: &protocol.WorkspaceClassesSettings{
341
Regular: "default",
342
Prebuild: "default",
343
},
344
RestrictedWorkspaceClasses: &[]string{"default"},
345
PrebuildSettings: &protocol.PrebuildSettings{
346
Enable: &b_false,
347
},
348
},
349
CreationTime: "2022-09-09T09:09:09.000Z",
350
}
351
352
if p.ID != "" {
353
result.ID = p.ID
354
}
355
if p.Name != "" {
356
result.Name = p.Name
357
}
358
if p.UserID != "" {
359
result.UserID = p.UserID
360
}
361
if p.TeamID != "" {
362
result.TeamID = p.TeamID
363
}
364
if p.CloneURL != "" {
365
result.CloneURL = p.CloneURL
366
}
367
if p.AppInstallationID != "" {
368
result.AppInstallationID = p.AppInstallationID
369
}
370
if p.Settings != nil {
371
result.Settings = p.Settings
372
}
373
374
return result
375
}
376
377