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/workspace_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
"net/http"
10
"net/http/httptest"
11
"testing"
12
"time"
13
14
"github.com/gitpod-io/gitpod/common-go/namegen"
15
16
fuzz "github.com/AdaLogics/go-fuzz-headers"
17
connect "github.com/bufbuild/connect-go"
18
"github.com/gitpod-io/gitpod/components/public-api/go/config"
19
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
20
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
21
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
22
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
23
"github.com/gitpod-io/gitpod/public-api-server/pkg/jws"
24
"github.com/gitpod-io/gitpod/public-api-server/pkg/jws/jwstest"
25
"github.com/golang/mock/gomock"
26
"github.com/google/go-cmp/cmp"
27
"github.com/sourcegraph/jsonrpc2"
28
"github.com/stretchr/testify/require"
29
"google.golang.org/protobuf/testing/protocmp"
30
"google.golang.org/protobuf/types/known/timestamppb"
31
)
32
33
func TestWorkspaceService_GetWorkspace(t *testing.T) {
34
35
workspaceID := workspaceTestData[0].Protocol.Workspace.ID
36
37
t.Run("invalid argument when workspace ID is missing", func(t *testing.T) {
38
_, client := setupWorkspacesService(t)
39
40
_, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{
41
WorkspaceId: "",
42
}))
43
require.Error(t, err)
44
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
45
})
46
47
t.Run("invalid argument when workspace ID does not validate", func(t *testing.T) {
48
_, client := setupWorkspacesService(t)
49
50
_, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{
51
WorkspaceId: "some-random-not-valid-workspace-id",
52
}))
53
require.Error(t, err)
54
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
55
})
56
57
t.Run("not found when workspace does not exist", func(t *testing.T) {
58
serverMock, client := setupWorkspacesService(t)
59
60
serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(nil, &jsonrpc2.Error{
61
Code: 404,
62
Message: "not found",
63
})
64
65
_, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{
66
WorkspaceId: workspaceID,
67
}))
68
require.Error(t, err)
69
require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
70
})
71
72
t.Run("returns a workspace when it exists", func(t *testing.T) {
73
serverMock, client := setupWorkspacesService(t)
74
75
serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(&workspaceTestData[0].Protocol, nil)
76
77
resp, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{
78
WorkspaceId: workspaceID,
79
}))
80
require.NoError(t, err)
81
82
requireEqualProto(t, workspaceTestData[0].API, resp.Msg.GetResult())
83
})
84
85
t.Run("returns a proper RecentFolders with when config.WorkspaceLocation exists", func(t *testing.T) {
86
serverMock, client := setupWorkspacesService(t)
87
88
wsInfo := workspaceTestData[0].Protocol
89
wsInfo.Workspace = nil
90
wsWorkspace := *workspaceTestData[0].Protocol.Workspace
91
wsWorkspace.Config = &protocol.WorkspaceConfig{
92
WorkspaceLocation: "gitpod/gitpod-ws.code-workspace",
93
}
94
wsInfo.Workspace = &wsWorkspace
95
96
serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(&wsInfo, nil)
97
98
resp, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{
99
WorkspaceId: workspaceID,
100
}))
101
require.NoError(t, err)
102
103
expectedWs := *workspaceTestData[0].API
104
expectedWs.Status = nil
105
expectedWsStatus := *workspaceTestData[0].API.Status
106
expectedWsStatus.Instance = nil
107
expectedInstance := *workspaceTestData[0].API.Status.Instance
108
expectedInstance.Status = nil
109
expectedInstanceStatus := *workspaceTestData[0].API.Status.Instance.Status
110
expectedInstanceStatus.RecentFolders = []string{"/workspace/gitpod/gitpod-ws.code-workspace"}
111
expectedInstance.Status = &expectedInstanceStatus
112
expectedWsStatus.Instance = &expectedInstance
113
expectedWs.Status = &expectedWsStatus
114
115
requireEqualProto(t, expectedWs, resp.Msg.GetResult())
116
})
117
118
t.Run("returns a proper RecentFolders with when config.CheckoutLocation exists", func(t *testing.T) {
119
serverMock, client := setupWorkspacesService(t)
120
121
wsInfo := workspaceTestData[0].Protocol
122
wsInfo.Workspace = nil
123
wsWorkspace := *workspaceTestData[0].Protocol.Workspace
124
wsWorkspace.Config = &protocol.WorkspaceConfig{
125
CheckoutLocation: "foo",
126
}
127
wsInfo.Workspace = &wsWorkspace
128
129
serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(&wsInfo, nil)
130
131
resp, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{
132
WorkspaceId: workspaceID,
133
}))
134
require.NoError(t, err)
135
136
expectedWs := *workspaceTestData[0].API
137
expectedWs.Status = nil
138
expectedWsStatus := *workspaceTestData[0].API.Status
139
expectedWsStatus.Instance = nil
140
expectedInstance := *workspaceTestData[0].API.Status.Instance
141
expectedInstance.Status = nil
142
expectedInstanceStatus := *workspaceTestData[0].API.Status.Instance.Status
143
expectedInstanceStatus.RecentFolders = []string{"/workspace/foo"}
144
expectedInstance.Status = &expectedInstanceStatus
145
expectedWsStatus.Instance = &expectedInstance
146
expectedWs.Status = &expectedWsStatus
147
148
requireEqualProto(t, expectedWs, resp.Msg.GetResult())
149
})
150
}
151
152
func TestWorkspaceService_StartWorkspace(t *testing.T) {
153
154
workspaceID := workspaceTestData[0].Protocol.Workspace.ID
155
156
t.Run("invalid argument when workspace ID is missing", func(t *testing.T) {
157
_, client := setupWorkspacesService(t)
158
159
_, err := client.StartWorkspace(context.Background(), connect.NewRequest(&v1.StartWorkspaceRequest{
160
WorkspaceId: "",
161
}))
162
require.Error(t, err)
163
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
164
})
165
166
t.Run("invalid argument when workspace ID does not validate", func(t *testing.T) {
167
_, client := setupWorkspacesService(t)
168
169
_, err := client.StartWorkspace(context.Background(), connect.NewRequest(&v1.StartWorkspaceRequest{
170
WorkspaceId: "some-random-not-valid-workspace-id",
171
}))
172
require.Error(t, err)
173
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
174
})
175
176
t.Run("not found when workspace does not exist", func(t *testing.T) {
177
serverMock, client := setupWorkspacesService(t)
178
179
serverMock.EXPECT().StartWorkspace(gomock.Any(), workspaceID, &protocol.StartWorkspaceOptions{}).Return(nil, &jsonrpc2.Error{
180
Code: 404,
181
Message: "not found",
182
})
183
184
_, err := client.StartWorkspace(context.Background(), connect.NewRequest(&v1.StartWorkspaceRequest{
185
WorkspaceId: workspaceID,
186
}))
187
require.Error(t, err)
188
require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
189
})
190
191
t.Run("delegates to server", func(t *testing.T) {
192
serverMock, client := setupWorkspacesService(t)
193
194
serverMock.EXPECT().StartWorkspace(gomock.Any(), workspaceID, &protocol.StartWorkspaceOptions{}).Return(&protocol.StartWorkspaceResult{
195
InstanceID: workspaceTestData[0].Protocol.LatestInstance.ID,
196
WorkspaceURL: workspaceTestData[0].Protocol.LatestInstance.IdeURL,
197
}, nil)
198
serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(&workspaceTestData[0].Protocol, nil)
199
200
resp, err := client.StartWorkspace(context.Background(), connect.NewRequest(&v1.StartWorkspaceRequest{
201
WorkspaceId: workspaceID,
202
}))
203
require.NoError(t, err)
204
205
requireEqualProto(t, workspaceTestData[0].API, resp.Msg.GetResult())
206
})
207
}
208
209
func TestWorkspaceService_StopWorkspace(t *testing.T) {
210
211
workspaceID := workspaceTestData[0].Protocol.Workspace.ID
212
213
t.Run("invalid argument when workspace ID is missing", func(t *testing.T) {
214
_, client := setupWorkspacesService(t)
215
216
_, err := client.StopWorkspace(context.Background(), connect.NewRequest(&v1.StopWorkspaceRequest{
217
WorkspaceId: "",
218
}))
219
require.Error(t, err)
220
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
221
})
222
223
t.Run("invalid argument when workspace ID does not validate", func(t *testing.T) {
224
_, client := setupWorkspacesService(t)
225
226
_, err := client.StopWorkspace(context.Background(), connect.NewRequest(&v1.StopWorkspaceRequest{
227
WorkspaceId: "some-random-not-valid-workspace-id",
228
}))
229
require.Error(t, err)
230
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
231
})
232
233
t.Run("not found when workspace does not exist", func(t *testing.T) {
234
serverMock, client := setupWorkspacesService(t)
235
236
serverMock.EXPECT().StopWorkspace(gomock.Any(), workspaceID).Return(&jsonrpc2.Error{
237
Code: 404,
238
Message: "not found",
239
})
240
241
_, err := client.StopWorkspace(context.Background(), connect.NewRequest(&v1.StopWorkspaceRequest{
242
WorkspaceId: workspaceID,
243
}))
244
require.Error(t, err)
245
require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
246
})
247
248
t.Run("delegates to server", func(t *testing.T) {
249
serverMock, client := setupWorkspacesService(t)
250
251
serverMock.EXPECT().StopWorkspace(gomock.Any(), workspaceID).Return(nil)
252
serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(&workspaceTestData[0].Protocol, nil)
253
254
resp, err := client.StopWorkspace(context.Background(), connect.NewRequest(&v1.StopWorkspaceRequest{
255
WorkspaceId: workspaceID,
256
}))
257
require.NoError(t, err)
258
259
requireEqualProto(t, workspaceTestData[0].API, resp.Msg.GetResult())
260
})
261
}
262
263
func TestWorkspaceService_DeleteWorkspace(t *testing.T) {
264
265
workspaceID := workspaceTestData[0].Protocol.Workspace.ID
266
267
t.Run("invalid argument when workspace ID is missing", func(t *testing.T) {
268
_, client := setupWorkspacesService(t)
269
270
_, err := client.DeleteWorkspace(context.Background(), connect.NewRequest(&v1.DeleteWorkspaceRequest{
271
WorkspaceId: "",
272
}))
273
require.Error(t, err)
274
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
275
})
276
277
t.Run("invalid argument when workspace ID does not validate", func(t *testing.T) {
278
_, client := setupWorkspacesService(t)
279
280
_, err := client.DeleteWorkspace(context.Background(), connect.NewRequest(&v1.DeleteWorkspaceRequest{
281
WorkspaceId: "some-random-not-valid-workspace-id",
282
}))
283
require.Error(t, err)
284
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
285
})
286
287
t.Run("not found when workspace does not exist", func(t *testing.T) {
288
serverMock, client := setupWorkspacesService(t)
289
290
serverMock.EXPECT().DeleteWorkspace(gomock.Any(), workspaceID).Return(&jsonrpc2.Error{
291
Code: 404,
292
Message: "not found",
293
})
294
295
_, err := client.DeleteWorkspace(context.Background(), connect.NewRequest(&v1.DeleteWorkspaceRequest{
296
WorkspaceId: workspaceID,
297
}))
298
require.Error(t, err)
299
require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
300
})
301
302
t.Run("delegates to server", func(t *testing.T) {
303
serverMock, client := setupWorkspacesService(t)
304
305
serverMock.EXPECT().DeleteWorkspace(gomock.Any(), workspaceID).Return(nil)
306
307
resp, err := client.DeleteWorkspace(context.Background(), connect.NewRequest(&v1.DeleteWorkspaceRequest{
308
WorkspaceId: workspaceID,
309
}))
310
require.NoError(t, err)
311
312
requireEqualProto(t, &v1.DeleteWorkspaceResponse{}, resp.Msg)
313
})
314
}
315
316
func TestWorkspaceService_GetOwnerToken(t *testing.T) {
317
const (
318
foundWorkspaceID = "easycz-seer-xl8o1zacpyw"
319
ownerToken = "some-owner-token"
320
)
321
322
type Expectation struct {
323
Code connect.Code
324
Response *v1.GetOwnerTokenResponse
325
}
326
tests := []struct {
327
name string
328
WorkspaceID string
329
Tokens map[string]string
330
Expect Expectation
331
}{
332
{
333
name: "returns an owner token when workspace is found by ID",
334
WorkspaceID: foundWorkspaceID,
335
Tokens: map[string]string{foundWorkspaceID: ownerToken},
336
Expect: Expectation{
337
Response: &v1.GetOwnerTokenResponse{
338
Token: ownerToken,
339
},
340
},
341
},
342
{
343
name: "not found when workspace is not found by ID",
344
WorkspaceID: mustGenerateWorkspaceID(t),
345
Expect: Expectation{
346
Code: connect.CodeNotFound,
347
},
348
},
349
}
350
351
for _, test := range tests {
352
t.Run(test.name, func(t *testing.T) {
353
serverMock, client := setupWorkspacesService(t)
354
355
serverMock.EXPECT().GetOwnerToken(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, workspaceID string) (res string, err error) {
356
w, ok := test.Tokens[workspaceID]
357
if !ok {
358
return "", &jsonrpc2.Error{
359
Code: 404,
360
Message: "not found",
361
}
362
}
363
return w, nil
364
})
365
366
resp, err := client.GetOwnerToken(context.Background(), connect.NewRequest(&v1.GetOwnerTokenRequest{
367
WorkspaceId: test.WorkspaceID,
368
}))
369
requireErrorCode(t, test.Expect.Code, err)
370
if test.Expect.Response != nil {
371
requireEqualProto(t, test.Expect.Response, resp.Msg)
372
}
373
})
374
}
375
}
376
377
func TestWorkspaceService_ListWorkspaces(t *testing.T) {
378
ctx := context.Background()
379
380
type Expectation struct {
381
Code connect.Code
382
Response *v1.ListWorkspacesResponse
383
}
384
385
tests := []struct {
386
Name string
387
Workspaces []*protocol.WorkspaceInfo
388
PageSize int32
389
Setup func(t *testing.T, srv *protocol.MockAPIInterface)
390
Expectation Expectation
391
}{
392
{
393
Name: "empty list",
394
Workspaces: []*protocol.WorkspaceInfo{},
395
Expectation: Expectation{
396
Response: &v1.ListWorkspacesResponse{},
397
},
398
},
399
{
400
Name: "valid workspaces",
401
Workspaces: []*protocol.WorkspaceInfo{
402
&workspaceTestData[0].Protocol,
403
},
404
Expectation: Expectation{
405
Response: &v1.ListWorkspacesResponse{
406
Result: []*v1.Workspace{
407
workspaceTestData[0].API,
408
},
409
},
410
},
411
},
412
{
413
Name: "invalid workspaces",
414
Workspaces: func() []*protocol.WorkspaceInfo {
415
ws := workspaceTestData[0].Protocol
416
wsi := *workspaceTestData[0].Protocol.LatestInstance
417
wsi.CreationTime = "invalid date"
418
ws.LatestInstance = &wsi
419
return []*protocol.WorkspaceInfo{&ws}
420
}(),
421
Expectation: Expectation{
422
Code: connect.CodeFailedPrecondition,
423
},
424
},
425
{
426
Name: "valid page size",
427
Setup: func(t *testing.T, srv *protocol.MockAPIInterface) {
428
srv.EXPECT().GetWorkspaces(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, options *protocol.GetWorkspacesOptions) (res []*protocol.WorkspaceInfo, err error) {
429
// Note: using to gomock argument matcher causes the test to block indefinitely instead of failing.
430
if int(options.Limit) != 42 {
431
t.Errorf("public-api passed from limit: %f instead of 42", options.Limit)
432
}
433
return nil, nil
434
})
435
},
436
PageSize: 42,
437
Expectation: Expectation{
438
Response: &v1.ListWorkspacesResponse{},
439
},
440
},
441
{
442
Name: "excessive page size",
443
PageSize: 1000,
444
Expectation: Expectation{
445
Code: connect.CodeInvalidArgument,
446
},
447
},
448
}
449
450
for _, test := range tests {
451
t.Run(test.Name, func(t *testing.T) {
452
var pagination *v1.Pagination
453
if test.PageSize != 0 {
454
pagination = &v1.Pagination{PageSize: test.PageSize}
455
}
456
457
serverMock, client := setupWorkspacesService(t)
458
459
if test.Workspaces != nil {
460
serverMock.EXPECT().GetWorkspaces(gomock.Any(), gomock.Any()).Return(test.Workspaces, nil)
461
} else if test.Setup != nil {
462
test.Setup(t, serverMock)
463
}
464
465
resp, err := client.ListWorkspaces(ctx, connect.NewRequest(&v1.ListWorkspacesRequest{
466
Pagination: pagination,
467
}))
468
requireErrorCode(t, test.Expectation.Code, err)
469
470
if test.Expectation.Response != nil {
471
if diff := cmp.Diff(test.Expectation.Response, resp.Msg, protocmp.Transform()); diff != "" {
472
t.Errorf("unexpected difference:\n%v", diff)
473
}
474
}
475
})
476
}
477
}
478
479
func TestWorkspaceService_StreamWorkspaceStatus(t *testing.T) {
480
const (
481
workspaceID = "easycz-seer-xl8o1zacpyw"
482
instanceID = "f2effcfd-3ddb-4187-b584-256e88a42442"
483
ownerToken = "some-owner-token"
484
)
485
486
t.Run("not found when workspace does not exist", func(t *testing.T) {
487
serverMock, client := setupWorkspacesService(t)
488
489
serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(nil, &jsonrpc2.Error{
490
Code: 404,
491
Message: "not found",
492
})
493
494
resp, _ := client.StreamWorkspaceStatus(context.Background(), connect.NewRequest(&v1.StreamWorkspaceStatusRequest{
495
WorkspaceId: workspaceID,
496
}))
497
498
resp.Receive()
499
500
require.Error(t, resp.Err())
501
require.Equal(t, connect.CodeNotFound, connect.CodeOf(resp.Err()))
502
})
503
504
t.Run("returns a workspace status", func(t *testing.T) {
505
serverMock, client := setupWorkspacesService(t)
506
507
serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(&workspaceTestData[0].Protocol, nil)
508
serverMock.EXPECT().WorkspaceUpdates(gomock.Any(), workspaceID).DoAndReturn(func(ctx context.Context, workspaceID string) (<-chan *protocol.WorkspaceInstance, error) {
509
ch := make(chan *protocol.WorkspaceInstance)
510
go func() {
511
ch <- workspaceTestData[0].Protocol.LatestInstance
512
}()
513
go func() {
514
<-ctx.Done()
515
close(ch)
516
}()
517
return ch, nil
518
})
519
520
ctx, cancel := context.WithCancel(context.Background())
521
resp, err := client.StreamWorkspaceStatus(ctx, connect.NewRequest(&v1.StreamWorkspaceStatusRequest{
522
WorkspaceId: workspaceID,
523
}))
524
525
require.NoError(t, err)
526
527
resp.Receive()
528
cancel()
529
530
requireEqualProto(t, workspaceTestData[0].API.Status, resp.Msg().Result)
531
})
532
}
533
534
func TestClientServerStreamInterceptor(t *testing.T) {
535
testInterceptor := &TestInterceptor{
536
expectedToken: "auth-token",
537
t: t,
538
}
539
540
ctrl := gomock.NewController(t)
541
t.Cleanup(ctrl.Finish)
542
543
serverMock := protocol.NewMockAPIInterface(ctrl)
544
545
svc := NewWorkspaceService(&FakeServerConnPool{
546
api: serverMock,
547
}, nil)
548
549
keyset := jwstest.GenerateKeySet(t)
550
rsa256, err := jws.NewRSA256(keyset)
551
require.NoError(t, err)
552
553
_, handler := v1connect.NewWorkspacesServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor(config.SessionConfig{
554
Issuer: "unitetest.com",
555
Cookie: config.CookieConfig{
556
Name: "cookie_jwt",
557
},
558
}, rsa256), testInterceptor))
559
560
srv := httptest.NewServer(handler)
561
t.Cleanup(srv.Close)
562
563
client := v1connect.NewWorkspacesServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(
564
auth.NewClientInterceptor("auth-token"),
565
testInterceptor,
566
))
567
568
resp, _ := client.StreamWorkspaceStatus(context.Background(), connect.NewRequest(&v1.StreamWorkspaceStatusRequest{
569
WorkspaceId: "",
570
}))
571
572
resp.Close()
573
}
574
575
func TestWorkspaceService_ListWorkspaceClasses(t *testing.T) {
576
577
t.Run("proxies request to server", func(t *testing.T) {
578
serverMock, client := setupWorkspacesService(t)
579
580
serverMock.EXPECT().GetSupportedWorkspaceClasses(gomock.Any()).Return([]*protocol.SupportedWorkspaceClass{
581
{
582
ID: "smol",
583
DisplayName: "Tiny",
584
Description: "The littlest there is",
585
IsDefault: true,
586
},
587
{
588
ID: "big",
589
DisplayName: "Huge",
590
Description: "The biggest there is",
591
IsDefault: false,
592
}}, nil)
593
594
retrieved, err := client.ListWorkspaceClasses(context.Background(), connect.NewRequest(&v1.ListWorkspaceClassesRequest{}))
595
require.NoError(t, err)
596
requireEqualProto(t, &v1.ListWorkspaceClassesResponse{
597
Result: []*v1.WorkspaceClass{
598
{
599
Id: "smol",
600
DisplayName: "Tiny",
601
Description: "The littlest there is",
602
IsDefault: true,
603
},
604
{
605
Id: "big",
606
DisplayName: "Huge",
607
Description: "The biggest there is",
608
IsDefault: false,
609
},
610
},
611
}, retrieved.Msg)
612
})
613
}
614
615
type TestInterceptor struct {
616
expectedToken string
617
t *testing.T
618
}
619
620
func (ti *TestInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {
621
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
622
return next(ctx, req)
623
}
624
}
625
626
func (ti *TestInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {
627
return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn {
628
token, _ := auth.TokenFromContext(ctx)
629
require.Equal(ti.t, ti.expectedToken, token.Value)
630
return next(ctx, spec)
631
}
632
}
633
634
func (ti *TestInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {
635
return func(ctx context.Context, conn connect.StreamingHandlerConn) error {
636
token, _ := auth.TokenFromContext(ctx)
637
require.Equal(ti.t, ti.expectedToken, token.Value)
638
return next(ctx, conn)
639
}
640
}
641
642
type workspaceTestDataEntry struct {
643
Name string
644
Protocol protocol.WorkspaceInfo
645
API *v1.Workspace
646
}
647
648
var workspaceTestData = []workspaceTestDataEntry{
649
{
650
Name: "comprehensive",
651
Protocol: protocol.WorkspaceInfo{
652
Workspace: &protocol.Workspace{
653
BaseImageNameResolved: "foo:bar",
654
ID: "gitpodio-gitpod-isq6xj458lj",
655
OwnerID: "fake-owner-id",
656
ContextURL: "open-prebuild/126ac54a-5922-4a45-9a18-670b057bf540/https://github.com/gitpod-io/gitpod/pull/18291",
657
Context: &protocol.WorkspaceContext{
658
NormalizedContextURL: "https://github.com/gitpod-io/gitpod/pull/18291",
659
Title: "tes ttitle",
660
Repository: &protocol.Repository{
661
Host: "github.com",
662
Name: "gitpod",
663
},
664
},
665
Description: "test description",
666
},
667
LatestInstance: &protocol.WorkspaceInstance{
668
ID: "f2effcfd-3ddb-4187-b584-256e88a42442",
669
IdeURL: "https://gitpodio-gitpod-isq6xj458lj.ws-eu53.protocol.io/",
670
CreationTime: "2022-07-12T10:04:49+0000",
671
WorkspaceID: "gitpodio-gitpod-isq6xj458lj",
672
Status: &protocol.WorkspaceInstanceStatus{
673
Conditions: &protocol.WorkspaceInstanceConditions{
674
Failed: "nope",
675
FirstUserActivity: "2022-07-12T10:04:49+0000",
676
Timeout: "nada",
677
},
678
Message: "has no message",
679
Phase: "running",
680
Version: 42,
681
ExposedPorts: []*protocol.WorkspaceInstancePort{
682
{
683
Port: 9000,
684
URL: "https://9000-gitpodio-gitpod-isq6xj458lj.ws-eu53.protocol.io",
685
Visibility: protocol.PortVisibilityPublic,
686
Protocol: protocol.PortProtocolHTTP,
687
},
688
{
689
Port: 9001,
690
URL: "https://9001-gitpodio-gitpod-isq6xj458lj.ws-eu53.protocol.io",
691
Visibility: protocol.PortVisibilityPrivate,
692
Protocol: protocol.PortProtocolHTTPS,
693
},
694
},
695
},
696
},
697
},
698
API: &v1.Workspace{
699
WorkspaceId: "gitpodio-gitpod-isq6xj458lj",
700
OwnerId: "fake-owner-id",
701
Context: &v1.WorkspaceContext{
702
ContextUrl: "open-prebuild/126ac54a-5922-4a45-9a18-670b057bf540/https://github.com/gitpod-io/gitpod/pull/18291",
703
Details: &v1.WorkspaceContext_Git_{
704
Git: &v1.WorkspaceContext_Git{
705
NormalizedContextUrl: "https://github.com/gitpod-io/gitpod/pull/18291",
706
Repository: &v1.WorkspaceContext_Repository{
707
Name: "gitpod",
708
},
709
},
710
},
711
},
712
Description: "test description",
713
Status: &v1.WorkspaceStatus{
714
Instance: &v1.WorkspaceInstance{
715
InstanceId: "f2effcfd-3ddb-4187-b584-256e88a42442",
716
WorkspaceId: "gitpodio-gitpod-isq6xj458lj",
717
CreatedAt: timestamppb.New(must(time.Parse(time.RFC3339, "2022-07-12T10:04:49Z"))),
718
Status: &v1.WorkspaceInstanceStatus{
719
StatusVersion: 42,
720
Phase: v1.WorkspaceInstanceStatus_PHASE_RUNNING,
721
Conditions: &v1.WorkspaceInstanceStatus_Conditions{
722
Failed: "nope",
723
Timeout: "nada",
724
FirstUserActivity: timestamppb.New(must(time.Parse(time.RFC3339, "2022-07-12T10:04:49Z"))),
725
},
726
Message: "has no message",
727
Url: "https://gitpodio-gitpod-isq6xj458lj.ws-eu53.protocol.io/",
728
Admission: v1.AdmissionLevel_ADMISSION_LEVEL_OWNER_ONLY,
729
Ports: []*v1.Port{
730
{
731
Port: 9000,
732
Policy: v1.PortPolicy_PORT_POLICY_PUBLIC,
733
Url: "https://9000-gitpodio-gitpod-isq6xj458lj.ws-eu53.protocol.io",
734
Protocol: v1.PortProtocol_PORT_PROTOCOL_HTTP,
735
},
736
{
737
Port: 9001,
738
Policy: v1.PortPolicy_PORT_POLICY_PRIVATE,
739
Url: "https://9001-gitpodio-gitpod-isq6xj458lj.ws-eu53.protocol.io",
740
Protocol: v1.PortProtocol_PORT_PROTOCOL_HTTPS,
741
},
742
},
743
RecentFolders: []string{"/workspace/gitpod"},
744
},
745
},
746
},
747
},
748
},
749
}
750
751
func TestConvertWorkspaceInfo(t *testing.T) {
752
type Expectation struct {
753
Result *v1.Workspace
754
Error string
755
}
756
tests := []struct {
757
Name string
758
Input protocol.WorkspaceInfo
759
Expectation Expectation
760
}{
761
{
762
Name: "happy path",
763
Input: workspaceTestData[0].Protocol,
764
Expectation: Expectation{Result: workspaceTestData[0].API},
765
},
766
}
767
768
for _, test := range tests {
769
t.Run(test.Name, func(t *testing.T) {
770
var (
771
act Expectation
772
err error
773
)
774
act.Result, err = convertWorkspaceInfo(&test.Input)
775
if err != nil {
776
act.Error = err.Error()
777
}
778
779
if diff := cmp.Diff(test.Expectation, act, protocmp.Transform()); diff != "" {
780
t.Errorf("unexpected convertWorkspaceInfo (-want +got):\n%s", diff)
781
}
782
})
783
}
784
}
785
786
func FuzzConvertWorkspaceInfo(f *testing.F) {
787
f.Fuzz(func(t *testing.T, data []byte) {
788
var nfo protocol.WorkspaceInfo
789
err := fuzz.NewConsumer(data).GenerateStruct(&nfo)
790
if err != nil {
791
return
792
}
793
794
// we really just care for panics
795
_, _ = convertWorkspaceInfo(&nfo)
796
})
797
}
798
799
func must[T any](t T, err error) T {
800
if err != nil {
801
panic(err)
802
}
803
return t
804
}
805
806
func setupWorkspacesService(t *testing.T) (*protocol.MockAPIInterface, v1connect.WorkspacesServiceClient) {
807
t.Helper()
808
809
ctrl := gomock.NewController(t)
810
t.Cleanup(ctrl.Finish)
811
812
serverMock := protocol.NewMockAPIInterface(ctrl)
813
814
svc := NewWorkspaceService(&FakeServerConnPool{
815
api: serverMock,
816
}, nil)
817
818
keyset := jwstest.GenerateKeySet(t)
819
rsa256, err := jws.NewRSA256(keyset)
820
require.NoError(t, err)
821
822
_, handler := v1connect.NewWorkspacesServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor(config.SessionConfig{
823
Issuer: "unitetest.com",
824
Cookie: config.CookieConfig{
825
Name: "cookie_jwt",
826
},
827
}, rsa256)))
828
829
srv := httptest.NewServer(handler)
830
t.Cleanup(srv.Close)
831
832
client := v1connect.NewWorkspacesServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(
833
auth.NewClientInterceptor("auth-token"),
834
))
835
836
return serverMock, client
837
}
838
839
type FakeServerConnPool struct {
840
api protocol.APIInterface
841
}
842
843
func (f *FakeServerConnPool) Get(ctx context.Context, token auth.Token) (protocol.APIInterface, error) {
844
return f.api, nil
845
}
846
847
func requireErrorCode(t *testing.T, expected connect.Code, err error) {
848
t.Helper()
849
if expected == 0 && err == nil {
850
return
851
}
852
853
actual := connect.CodeOf(err)
854
require.Equal(t, expected, actual, "expected code %s, but got %s from error %v", expected.String(), actual.String(), err)
855
}
856
857
func mustGenerateWorkspaceID(t *testing.T) string {
858
t.Helper()
859
860
wsid, err := namegen.GenerateWorkspaceID()
861
require.NoError(t, err)
862
863
return wsid
864
}
865
866