Path: blob/main/components/public-api-server/pkg/apiv1/workspace_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"11"time"1213"github.com/gitpod-io/gitpod/common-go/namegen"1415fuzz "github.com/AdaLogics/go-fuzz-headers"16connect "github.com/bufbuild/connect-go"17"github.com/gitpod-io/gitpod/components/public-api/go/config"18v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"19"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"20protocol "github.com/gitpod-io/gitpod/gitpod-protocol"21"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"22"github.com/gitpod-io/gitpod/public-api-server/pkg/jws"23"github.com/gitpod-io/gitpod/public-api-server/pkg/jws/jwstest"24"github.com/golang/mock/gomock"25"github.com/google/go-cmp/cmp"26"github.com/sourcegraph/jsonrpc2"27"github.com/stretchr/testify/require"28"google.golang.org/protobuf/testing/protocmp"29"google.golang.org/protobuf/types/known/timestamppb"30)3132func TestWorkspaceService_GetWorkspace(t *testing.T) {3334workspaceID := workspaceTestData[0].Protocol.Workspace.ID3536t.Run("invalid argument when workspace ID is missing", func(t *testing.T) {37_, client := setupWorkspacesService(t)3839_, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{40WorkspaceId: "",41}))42require.Error(t, err)43require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))44})4546t.Run("invalid argument when workspace ID does not validate", func(t *testing.T) {47_, client := setupWorkspacesService(t)4849_, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{50WorkspaceId: "some-random-not-valid-workspace-id",51}))52require.Error(t, err)53require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))54})5556t.Run("not found when workspace does not exist", func(t *testing.T) {57serverMock, client := setupWorkspacesService(t)5859serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(nil, &jsonrpc2.Error{60Code: 404,61Message: "not found",62})6364_, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{65WorkspaceId: workspaceID,66}))67require.Error(t, err)68require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))69})7071t.Run("returns a workspace when it exists", func(t *testing.T) {72serverMock, client := setupWorkspacesService(t)7374serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(&workspaceTestData[0].Protocol, nil)7576resp, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{77WorkspaceId: workspaceID,78}))79require.NoError(t, err)8081requireEqualProto(t, workspaceTestData[0].API, resp.Msg.GetResult())82})8384t.Run("returns a proper RecentFolders with when config.WorkspaceLocation exists", func(t *testing.T) {85serverMock, client := setupWorkspacesService(t)8687wsInfo := workspaceTestData[0].Protocol88wsInfo.Workspace = nil89wsWorkspace := *workspaceTestData[0].Protocol.Workspace90wsWorkspace.Config = &protocol.WorkspaceConfig{91WorkspaceLocation: "gitpod/gitpod-ws.code-workspace",92}93wsInfo.Workspace = &wsWorkspace9495serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(&wsInfo, nil)9697resp, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{98WorkspaceId: workspaceID,99}))100require.NoError(t, err)101102expectedWs := *workspaceTestData[0].API103expectedWs.Status = nil104expectedWsStatus := *workspaceTestData[0].API.Status105expectedWsStatus.Instance = nil106expectedInstance := *workspaceTestData[0].API.Status.Instance107expectedInstance.Status = nil108expectedInstanceStatus := *workspaceTestData[0].API.Status.Instance.Status109expectedInstanceStatus.RecentFolders = []string{"/workspace/gitpod/gitpod-ws.code-workspace"}110expectedInstance.Status = &expectedInstanceStatus111expectedWsStatus.Instance = &expectedInstance112expectedWs.Status = &expectedWsStatus113114requireEqualProto(t, expectedWs, resp.Msg.GetResult())115})116117t.Run("returns a proper RecentFolders with when config.CheckoutLocation exists", func(t *testing.T) {118serverMock, client := setupWorkspacesService(t)119120wsInfo := workspaceTestData[0].Protocol121wsInfo.Workspace = nil122wsWorkspace := *workspaceTestData[0].Protocol.Workspace123wsWorkspace.Config = &protocol.WorkspaceConfig{124CheckoutLocation: "foo",125}126wsInfo.Workspace = &wsWorkspace127128serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(&wsInfo, nil)129130resp, err := client.GetWorkspace(context.Background(), connect.NewRequest(&v1.GetWorkspaceRequest{131WorkspaceId: workspaceID,132}))133require.NoError(t, err)134135expectedWs := *workspaceTestData[0].API136expectedWs.Status = nil137expectedWsStatus := *workspaceTestData[0].API.Status138expectedWsStatus.Instance = nil139expectedInstance := *workspaceTestData[0].API.Status.Instance140expectedInstance.Status = nil141expectedInstanceStatus := *workspaceTestData[0].API.Status.Instance.Status142expectedInstanceStatus.RecentFolders = []string{"/workspace/foo"}143expectedInstance.Status = &expectedInstanceStatus144expectedWsStatus.Instance = &expectedInstance145expectedWs.Status = &expectedWsStatus146147requireEqualProto(t, expectedWs, resp.Msg.GetResult())148})149}150151func TestWorkspaceService_StartWorkspace(t *testing.T) {152153workspaceID := workspaceTestData[0].Protocol.Workspace.ID154155t.Run("invalid argument when workspace ID is missing", func(t *testing.T) {156_, client := setupWorkspacesService(t)157158_, err := client.StartWorkspace(context.Background(), connect.NewRequest(&v1.StartWorkspaceRequest{159WorkspaceId: "",160}))161require.Error(t, err)162require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))163})164165t.Run("invalid argument when workspace ID does not validate", func(t *testing.T) {166_, client := setupWorkspacesService(t)167168_, err := client.StartWorkspace(context.Background(), connect.NewRequest(&v1.StartWorkspaceRequest{169WorkspaceId: "some-random-not-valid-workspace-id",170}))171require.Error(t, err)172require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))173})174175t.Run("not found when workspace does not exist", func(t *testing.T) {176serverMock, client := setupWorkspacesService(t)177178serverMock.EXPECT().StartWorkspace(gomock.Any(), workspaceID, &protocol.StartWorkspaceOptions{}).Return(nil, &jsonrpc2.Error{179Code: 404,180Message: "not found",181})182183_, err := client.StartWorkspace(context.Background(), connect.NewRequest(&v1.StartWorkspaceRequest{184WorkspaceId: workspaceID,185}))186require.Error(t, err)187require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))188})189190t.Run("delegates to server", func(t *testing.T) {191serverMock, client := setupWorkspacesService(t)192193serverMock.EXPECT().StartWorkspace(gomock.Any(), workspaceID, &protocol.StartWorkspaceOptions{}).Return(&protocol.StartWorkspaceResult{194InstanceID: workspaceTestData[0].Protocol.LatestInstance.ID,195WorkspaceURL: workspaceTestData[0].Protocol.LatestInstance.IdeURL,196}, nil)197serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(&workspaceTestData[0].Protocol, nil)198199resp, err := client.StartWorkspace(context.Background(), connect.NewRequest(&v1.StartWorkspaceRequest{200WorkspaceId: workspaceID,201}))202require.NoError(t, err)203204requireEqualProto(t, workspaceTestData[0].API, resp.Msg.GetResult())205})206}207208func TestWorkspaceService_StopWorkspace(t *testing.T) {209210workspaceID := workspaceTestData[0].Protocol.Workspace.ID211212t.Run("invalid argument when workspace ID is missing", func(t *testing.T) {213_, client := setupWorkspacesService(t)214215_, err := client.StopWorkspace(context.Background(), connect.NewRequest(&v1.StopWorkspaceRequest{216WorkspaceId: "",217}))218require.Error(t, err)219require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))220})221222t.Run("invalid argument when workspace ID does not validate", func(t *testing.T) {223_, client := setupWorkspacesService(t)224225_, err := client.StopWorkspace(context.Background(), connect.NewRequest(&v1.StopWorkspaceRequest{226WorkspaceId: "some-random-not-valid-workspace-id",227}))228require.Error(t, err)229require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))230})231232t.Run("not found when workspace does not exist", func(t *testing.T) {233serverMock, client := setupWorkspacesService(t)234235serverMock.EXPECT().StopWorkspace(gomock.Any(), workspaceID).Return(&jsonrpc2.Error{236Code: 404,237Message: "not found",238})239240_, err := client.StopWorkspace(context.Background(), connect.NewRequest(&v1.StopWorkspaceRequest{241WorkspaceId: workspaceID,242}))243require.Error(t, err)244require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))245})246247t.Run("delegates to server", func(t *testing.T) {248serverMock, client := setupWorkspacesService(t)249250serverMock.EXPECT().StopWorkspace(gomock.Any(), workspaceID).Return(nil)251serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(&workspaceTestData[0].Protocol, nil)252253resp, err := client.StopWorkspace(context.Background(), connect.NewRequest(&v1.StopWorkspaceRequest{254WorkspaceId: workspaceID,255}))256require.NoError(t, err)257258requireEqualProto(t, workspaceTestData[0].API, resp.Msg.GetResult())259})260}261262func TestWorkspaceService_DeleteWorkspace(t *testing.T) {263264workspaceID := workspaceTestData[0].Protocol.Workspace.ID265266t.Run("invalid argument when workspace ID is missing", func(t *testing.T) {267_, client := setupWorkspacesService(t)268269_, err := client.DeleteWorkspace(context.Background(), connect.NewRequest(&v1.DeleteWorkspaceRequest{270WorkspaceId: "",271}))272require.Error(t, err)273require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))274})275276t.Run("invalid argument when workspace ID does not validate", func(t *testing.T) {277_, client := setupWorkspacesService(t)278279_, err := client.DeleteWorkspace(context.Background(), connect.NewRequest(&v1.DeleteWorkspaceRequest{280WorkspaceId: "some-random-not-valid-workspace-id",281}))282require.Error(t, err)283require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))284})285286t.Run("not found when workspace does not exist", func(t *testing.T) {287serverMock, client := setupWorkspacesService(t)288289serverMock.EXPECT().DeleteWorkspace(gomock.Any(), workspaceID).Return(&jsonrpc2.Error{290Code: 404,291Message: "not found",292})293294_, err := client.DeleteWorkspace(context.Background(), connect.NewRequest(&v1.DeleteWorkspaceRequest{295WorkspaceId: workspaceID,296}))297require.Error(t, err)298require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))299})300301t.Run("delegates to server", func(t *testing.T) {302serverMock, client := setupWorkspacesService(t)303304serverMock.EXPECT().DeleteWorkspace(gomock.Any(), workspaceID).Return(nil)305306resp, err := client.DeleteWorkspace(context.Background(), connect.NewRequest(&v1.DeleteWorkspaceRequest{307WorkspaceId: workspaceID,308}))309require.NoError(t, err)310311requireEqualProto(t, &v1.DeleteWorkspaceResponse{}, resp.Msg)312})313}314315func TestWorkspaceService_GetOwnerToken(t *testing.T) {316const (317foundWorkspaceID = "easycz-seer-xl8o1zacpyw"318ownerToken = "some-owner-token"319)320321type Expectation struct {322Code connect.Code323Response *v1.GetOwnerTokenResponse324}325tests := []struct {326name string327WorkspaceID string328Tokens map[string]string329Expect Expectation330}{331{332name: "returns an owner token when workspace is found by ID",333WorkspaceID: foundWorkspaceID,334Tokens: map[string]string{foundWorkspaceID: ownerToken},335Expect: Expectation{336Response: &v1.GetOwnerTokenResponse{337Token: ownerToken,338},339},340},341{342name: "not found when workspace is not found by ID",343WorkspaceID: mustGenerateWorkspaceID(t),344Expect: Expectation{345Code: connect.CodeNotFound,346},347},348}349350for _, test := range tests {351t.Run(test.name, func(t *testing.T) {352serverMock, client := setupWorkspacesService(t)353354serverMock.EXPECT().GetOwnerToken(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, workspaceID string) (res string, err error) {355w, ok := test.Tokens[workspaceID]356if !ok {357return "", &jsonrpc2.Error{358Code: 404,359Message: "not found",360}361}362return w, nil363})364365resp, err := client.GetOwnerToken(context.Background(), connect.NewRequest(&v1.GetOwnerTokenRequest{366WorkspaceId: test.WorkspaceID,367}))368requireErrorCode(t, test.Expect.Code, err)369if test.Expect.Response != nil {370requireEqualProto(t, test.Expect.Response, resp.Msg)371}372})373}374}375376func TestWorkspaceService_ListWorkspaces(t *testing.T) {377ctx := context.Background()378379type Expectation struct {380Code connect.Code381Response *v1.ListWorkspacesResponse382}383384tests := []struct {385Name string386Workspaces []*protocol.WorkspaceInfo387PageSize int32388Setup func(t *testing.T, srv *protocol.MockAPIInterface)389Expectation Expectation390}{391{392Name: "empty list",393Workspaces: []*protocol.WorkspaceInfo{},394Expectation: Expectation{395Response: &v1.ListWorkspacesResponse{},396},397},398{399Name: "valid workspaces",400Workspaces: []*protocol.WorkspaceInfo{401&workspaceTestData[0].Protocol,402},403Expectation: Expectation{404Response: &v1.ListWorkspacesResponse{405Result: []*v1.Workspace{406workspaceTestData[0].API,407},408},409},410},411{412Name: "invalid workspaces",413Workspaces: func() []*protocol.WorkspaceInfo {414ws := workspaceTestData[0].Protocol415wsi := *workspaceTestData[0].Protocol.LatestInstance416wsi.CreationTime = "invalid date"417ws.LatestInstance = &wsi418return []*protocol.WorkspaceInfo{&ws}419}(),420Expectation: Expectation{421Code: connect.CodeFailedPrecondition,422},423},424{425Name: "valid page size",426Setup: func(t *testing.T, srv *protocol.MockAPIInterface) {427srv.EXPECT().GetWorkspaces(gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, options *protocol.GetWorkspacesOptions) (res []*protocol.WorkspaceInfo, err error) {428// Note: using to gomock argument matcher causes the test to block indefinitely instead of failing.429if int(options.Limit) != 42 {430t.Errorf("public-api passed from limit: %f instead of 42", options.Limit)431}432return nil, nil433})434},435PageSize: 42,436Expectation: Expectation{437Response: &v1.ListWorkspacesResponse{},438},439},440{441Name: "excessive page size",442PageSize: 1000,443Expectation: Expectation{444Code: connect.CodeInvalidArgument,445},446},447}448449for _, test := range tests {450t.Run(test.Name, func(t *testing.T) {451var pagination *v1.Pagination452if test.PageSize != 0 {453pagination = &v1.Pagination{PageSize: test.PageSize}454}455456serverMock, client := setupWorkspacesService(t)457458if test.Workspaces != nil {459serverMock.EXPECT().GetWorkspaces(gomock.Any(), gomock.Any()).Return(test.Workspaces, nil)460} else if test.Setup != nil {461test.Setup(t, serverMock)462}463464resp, err := client.ListWorkspaces(ctx, connect.NewRequest(&v1.ListWorkspacesRequest{465Pagination: pagination,466}))467requireErrorCode(t, test.Expectation.Code, err)468469if test.Expectation.Response != nil {470if diff := cmp.Diff(test.Expectation.Response, resp.Msg, protocmp.Transform()); diff != "" {471t.Errorf("unexpected difference:\n%v", diff)472}473}474})475}476}477478func TestWorkspaceService_StreamWorkspaceStatus(t *testing.T) {479const (480workspaceID = "easycz-seer-xl8o1zacpyw"481instanceID = "f2effcfd-3ddb-4187-b584-256e88a42442"482ownerToken = "some-owner-token"483)484485t.Run("not found when workspace does not exist", func(t *testing.T) {486serverMock, client := setupWorkspacesService(t)487488serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(nil, &jsonrpc2.Error{489Code: 404,490Message: "not found",491})492493resp, _ := client.StreamWorkspaceStatus(context.Background(), connect.NewRequest(&v1.StreamWorkspaceStatusRequest{494WorkspaceId: workspaceID,495}))496497resp.Receive()498499require.Error(t, resp.Err())500require.Equal(t, connect.CodeNotFound, connect.CodeOf(resp.Err()))501})502503t.Run("returns a workspace status", func(t *testing.T) {504serverMock, client := setupWorkspacesService(t)505506serverMock.EXPECT().GetWorkspace(gomock.Any(), workspaceID).Return(&workspaceTestData[0].Protocol, nil)507serverMock.EXPECT().WorkspaceUpdates(gomock.Any(), workspaceID).DoAndReturn(func(ctx context.Context, workspaceID string) (<-chan *protocol.WorkspaceInstance, error) {508ch := make(chan *protocol.WorkspaceInstance)509go func() {510ch <- workspaceTestData[0].Protocol.LatestInstance511}()512go func() {513<-ctx.Done()514close(ch)515}()516return ch, nil517})518519ctx, cancel := context.WithCancel(context.Background())520resp, err := client.StreamWorkspaceStatus(ctx, connect.NewRequest(&v1.StreamWorkspaceStatusRequest{521WorkspaceId: workspaceID,522}))523524require.NoError(t, err)525526resp.Receive()527cancel()528529requireEqualProto(t, workspaceTestData[0].API.Status, resp.Msg().Result)530})531}532533func TestClientServerStreamInterceptor(t *testing.T) {534testInterceptor := &TestInterceptor{535expectedToken: "auth-token",536t: t,537}538539ctrl := gomock.NewController(t)540t.Cleanup(ctrl.Finish)541542serverMock := protocol.NewMockAPIInterface(ctrl)543544svc := NewWorkspaceService(&FakeServerConnPool{545api: serverMock,546}, nil)547548keyset := jwstest.GenerateKeySet(t)549rsa256, err := jws.NewRSA256(keyset)550require.NoError(t, err)551552_, handler := v1connect.NewWorkspacesServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor(config.SessionConfig{553Issuer: "unitetest.com",554Cookie: config.CookieConfig{555Name: "cookie_jwt",556},557}, rsa256), testInterceptor))558559srv := httptest.NewServer(handler)560t.Cleanup(srv.Close)561562client := v1connect.NewWorkspacesServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(563auth.NewClientInterceptor("auth-token"),564testInterceptor,565))566567resp, _ := client.StreamWorkspaceStatus(context.Background(), connect.NewRequest(&v1.StreamWorkspaceStatusRequest{568WorkspaceId: "",569}))570571resp.Close()572}573574func TestWorkspaceService_ListWorkspaceClasses(t *testing.T) {575576t.Run("proxies request to server", func(t *testing.T) {577serverMock, client := setupWorkspacesService(t)578579serverMock.EXPECT().GetSupportedWorkspaceClasses(gomock.Any()).Return([]*protocol.SupportedWorkspaceClass{580{581ID: "smol",582DisplayName: "Tiny",583Description: "The littlest there is",584IsDefault: true,585},586{587ID: "big",588DisplayName: "Huge",589Description: "The biggest there is",590IsDefault: false,591}}, nil)592593retrieved, err := client.ListWorkspaceClasses(context.Background(), connect.NewRequest(&v1.ListWorkspaceClassesRequest{}))594require.NoError(t, err)595requireEqualProto(t, &v1.ListWorkspaceClassesResponse{596Result: []*v1.WorkspaceClass{597{598Id: "smol",599DisplayName: "Tiny",600Description: "The littlest there is",601IsDefault: true,602},603{604Id: "big",605DisplayName: "Huge",606Description: "The biggest there is",607IsDefault: false,608},609},610}, retrieved.Msg)611})612}613614type TestInterceptor struct {615expectedToken string616t *testing.T617}618619func (ti *TestInterceptor) WrapUnary(next connect.UnaryFunc) connect.UnaryFunc {620return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {621return next(ctx, req)622}623}624625func (ti *TestInterceptor) WrapStreamingClient(next connect.StreamingClientFunc) connect.StreamingClientFunc {626return func(ctx context.Context, spec connect.Spec) connect.StreamingClientConn {627token, _ := auth.TokenFromContext(ctx)628require.Equal(ti.t, ti.expectedToken, token.Value)629return next(ctx, spec)630}631}632633func (ti *TestInterceptor) WrapStreamingHandler(next connect.StreamingHandlerFunc) connect.StreamingHandlerFunc {634return func(ctx context.Context, conn connect.StreamingHandlerConn) error {635token, _ := auth.TokenFromContext(ctx)636require.Equal(ti.t, ti.expectedToken, token.Value)637return next(ctx, conn)638}639}640641type workspaceTestDataEntry struct {642Name string643Protocol protocol.WorkspaceInfo644API *v1.Workspace645}646647var workspaceTestData = []workspaceTestDataEntry{648{649Name: "comprehensive",650Protocol: protocol.WorkspaceInfo{651Workspace: &protocol.Workspace{652BaseImageNameResolved: "foo:bar",653ID: "gitpodio-gitpod-isq6xj458lj",654OwnerID: "fake-owner-id",655ContextURL: "open-prebuild/126ac54a-5922-4a45-9a18-670b057bf540/https://github.com/gitpod-io/gitpod/pull/18291",656Context: &protocol.WorkspaceContext{657NormalizedContextURL: "https://github.com/gitpod-io/gitpod/pull/18291",658Title: "tes ttitle",659Repository: &protocol.Repository{660Host: "github.com",661Name: "gitpod",662},663},664Description: "test description",665},666LatestInstance: &protocol.WorkspaceInstance{667ID: "f2effcfd-3ddb-4187-b584-256e88a42442",668IdeURL: "https://gitpodio-gitpod-isq6xj458lj.ws-eu53.protocol.io/",669CreationTime: "2022-07-12T10:04:49+0000",670WorkspaceID: "gitpodio-gitpod-isq6xj458lj",671Status: &protocol.WorkspaceInstanceStatus{672Conditions: &protocol.WorkspaceInstanceConditions{673Failed: "nope",674FirstUserActivity: "2022-07-12T10:04:49+0000",675Timeout: "nada",676},677Message: "has no message",678Phase: "running",679Version: 42,680ExposedPorts: []*protocol.WorkspaceInstancePort{681{682Port: 9000,683URL: "https://9000-gitpodio-gitpod-isq6xj458lj.ws-eu53.protocol.io",684Visibility: protocol.PortVisibilityPublic,685Protocol: protocol.PortProtocolHTTP,686},687{688Port: 9001,689URL: "https://9001-gitpodio-gitpod-isq6xj458lj.ws-eu53.protocol.io",690Visibility: protocol.PortVisibilityPrivate,691Protocol: protocol.PortProtocolHTTPS,692},693},694},695},696},697API: &v1.Workspace{698WorkspaceId: "gitpodio-gitpod-isq6xj458lj",699OwnerId: "fake-owner-id",700Context: &v1.WorkspaceContext{701ContextUrl: "open-prebuild/126ac54a-5922-4a45-9a18-670b057bf540/https://github.com/gitpod-io/gitpod/pull/18291",702Details: &v1.WorkspaceContext_Git_{703Git: &v1.WorkspaceContext_Git{704NormalizedContextUrl: "https://github.com/gitpod-io/gitpod/pull/18291",705Repository: &v1.WorkspaceContext_Repository{706Name: "gitpod",707},708},709},710},711Description: "test description",712Status: &v1.WorkspaceStatus{713Instance: &v1.WorkspaceInstance{714InstanceId: "f2effcfd-3ddb-4187-b584-256e88a42442",715WorkspaceId: "gitpodio-gitpod-isq6xj458lj",716CreatedAt: timestamppb.New(must(time.Parse(time.RFC3339, "2022-07-12T10:04:49Z"))),717Status: &v1.WorkspaceInstanceStatus{718StatusVersion: 42,719Phase: v1.WorkspaceInstanceStatus_PHASE_RUNNING,720Conditions: &v1.WorkspaceInstanceStatus_Conditions{721Failed: "nope",722Timeout: "nada",723FirstUserActivity: timestamppb.New(must(time.Parse(time.RFC3339, "2022-07-12T10:04:49Z"))),724},725Message: "has no message",726Url: "https://gitpodio-gitpod-isq6xj458lj.ws-eu53.protocol.io/",727Admission: v1.AdmissionLevel_ADMISSION_LEVEL_OWNER_ONLY,728Ports: []*v1.Port{729{730Port: 9000,731Policy: v1.PortPolicy_PORT_POLICY_PUBLIC,732Url: "https://9000-gitpodio-gitpod-isq6xj458lj.ws-eu53.protocol.io",733Protocol: v1.PortProtocol_PORT_PROTOCOL_HTTP,734},735{736Port: 9001,737Policy: v1.PortPolicy_PORT_POLICY_PRIVATE,738Url: "https://9001-gitpodio-gitpod-isq6xj458lj.ws-eu53.protocol.io",739Protocol: v1.PortProtocol_PORT_PROTOCOL_HTTPS,740},741},742RecentFolders: []string{"/workspace/gitpod"},743},744},745},746},747},748}749750func TestConvertWorkspaceInfo(t *testing.T) {751type Expectation struct {752Result *v1.Workspace753Error string754}755tests := []struct {756Name string757Input protocol.WorkspaceInfo758Expectation Expectation759}{760{761Name: "happy path",762Input: workspaceTestData[0].Protocol,763Expectation: Expectation{Result: workspaceTestData[0].API},764},765}766767for _, test := range tests {768t.Run(test.Name, func(t *testing.T) {769var (770act Expectation771err error772)773act.Result, err = convertWorkspaceInfo(&test.Input)774if err != nil {775act.Error = err.Error()776}777778if diff := cmp.Diff(test.Expectation, act, protocmp.Transform()); diff != "" {779t.Errorf("unexpected convertWorkspaceInfo (-want +got):\n%s", diff)780}781})782}783}784785func FuzzConvertWorkspaceInfo(f *testing.F) {786f.Fuzz(func(t *testing.T, data []byte) {787var nfo protocol.WorkspaceInfo788err := fuzz.NewConsumer(data).GenerateStruct(&nfo)789if err != nil {790return791}792793// we really just care for panics794_, _ = convertWorkspaceInfo(&nfo)795})796}797798func must[T any](t T, err error) T {799if err != nil {800panic(err)801}802return t803}804805func setupWorkspacesService(t *testing.T) (*protocol.MockAPIInterface, v1connect.WorkspacesServiceClient) {806t.Helper()807808ctrl := gomock.NewController(t)809t.Cleanup(ctrl.Finish)810811serverMock := protocol.NewMockAPIInterface(ctrl)812813svc := NewWorkspaceService(&FakeServerConnPool{814api: serverMock,815}, nil)816817keyset := jwstest.GenerateKeySet(t)818rsa256, err := jws.NewRSA256(keyset)819require.NoError(t, err)820821_, handler := v1connect.NewWorkspacesServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor(config.SessionConfig{822Issuer: "unitetest.com",823Cookie: config.CookieConfig{824Name: "cookie_jwt",825},826}, rsa256)))827828srv := httptest.NewServer(handler)829t.Cleanup(srv.Close)830831client := v1connect.NewWorkspacesServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(832auth.NewClientInterceptor("auth-token"),833))834835return serverMock, client836}837838type FakeServerConnPool struct {839api protocol.APIInterface840}841842func (f *FakeServerConnPool) Get(ctx context.Context, token auth.Token) (protocol.APIInterface, error) {843return f.api, nil844}845846func requireErrorCode(t *testing.T, expected connect.Code, err error) {847t.Helper()848if expected == 0 && err == nil {849return850}851852actual := connect.CodeOf(err)853require.Equal(t, expected, actual, "expected code %s, but got %s from error %v", expected.String(), actual.String(), err)854}855856func mustGenerateWorkspaceID(t *testing.T) string {857t.Helper()858859wsid, err := namegen.GenerateWorkspaceID()860require.NoError(t, err)861862return wsid863}864865866