Path: blob/main/components/public-api-server/pkg/apiv1/project_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"fmt"9"math/rand"10"net/http"11"net/http/httptest"12"testing"1314connect "github.com/bufbuild/connect-go"15"github.com/gitpod-io/gitpod/components/public-api/go/config"16v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"17"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"18protocol "github.com/gitpod-io/gitpod/gitpod-protocol"19"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"20"github.com/gitpod-io/gitpod/public-api-server/pkg/jws"21"github.com/gitpod-io/gitpod/public-api-server/pkg/jws/jwstest"22"github.com/golang/mock/gomock"23"github.com/google/uuid"24"github.com/stretchr/testify/require"25)2627func TestProjectsService_CreateProject(t *testing.T) {2829t.Run("returns invalid argument when request validation fails", func(t *testing.T) {30for _, s := range []struct {31Name string32Spec *v1.Project33ExpectedError string34}{35{36Name: "name is required",37Spec: &v1.Project{},38ExpectedError: "Name is a required argument.",39},40{41Name: "whitespace name is rejected",42Spec: &v1.Project{43Name: " ",44},45ExpectedError: "Name is a required argument.",46},47{48Name: "clone url is required",49Spec: &v1.Project{50Name: "name",51},52ExpectedError: "Clone URL is a required argument.",53},54{55Name: "whitespace clone url is rejected",56Spec: &v1.Project{57Name: "name",58CloneUrl: " ",59},60ExpectedError: "Clone URL is a required argument.",61},62{63Name: "team ID must be a valid UUID",64Spec: &v1.Project{65Name: "name",66CloneUrl: "some.clone.url",67TeamId: "my-user",68},69ExpectedError: "Team ID is not a valid UUID.",70},71} {72t.Run(s.Name, func(t *testing.T) {73_, client := setupProjectsService(t)7475_, err := client.CreateProject(context.Background(), connect.NewRequest(&v1.CreateProjectRequest{76Project: s.Spec,77}))78require.Error(t, err)79require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))80require.Equal(t, s.ExpectedError, err.(*connect.Error).Message())81})82}83})8485t.Run("proxies request to server", func(t *testing.T) {86projectsMock, client := setupProjectsService(t)8788project := newProject(&protocol.Project{89Settings: &protocol.ProjectSettings{},90})9192projectsMock.EXPECT().CreateProject(gomock.Any(), &protocol.CreateProjectOptions{93TeamID: project.TeamID,94Name: project.Name,95CloneURL: project.CloneURL,96AppInstallationID: "undefined",97}).Return(project, nil)9899response, err := client.CreateProject(context.Background(), connect.NewRequest(&v1.CreateProjectRequest{100Project: &v1.Project{101Name: project.Name,102CloneUrl: project.CloneURL,103TeamId: project.TeamID,104},105}))106require.NoError(t, err)107requireEqualProto(t, &v1.CreateProjectResponse{108Project: projectToAPIResponse(project),109}, response.Msg)110111})112}113114func TestProjectsService_ListProjects(t *testing.T) {115116t.Run("invalid argument when org id is missing", func(t *testing.T) {117_, client := setupProjectsService(t)118119_, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{}))120require.Error(t, err)121require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))122})123124t.Run("invalid argument when org ID is not a valid UUID", func(t *testing.T) {125_, client := setupProjectsService(t)126127_, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{128TeamId: "some-id",129}))130require.Error(t, err)131require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))132})133134t.Run("no projects from server return empty list", func(t *testing.T) {135serverMock, client := setupProjectsService(t)136137teamID := uuid.New().String()138serverMock.EXPECT().GetTeamProjects(gomock.Any(), teamID).Return(nil, nil)139140response, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{141TeamId: teamID,142}))143require.NoError(t, err)144requireEqualProto(t, &v1.ListProjectsResponse{145Projects: nil,146TotalResults: 0,147}, response.Msg)148})149150t.Run("retrieves projects for team and paginates", func(t *testing.T) {151serverMock, client := setupProjectsService(t)152153projects := []*protocol.Project{154newProject(&protocol.Project{}),155newProject(&protocol.Project{}),156newProject(&protocol.Project{}),157newProject(&protocol.Project{}),158newProject(&protocol.Project{}),159}160161teamID := uuid.New().String()162serverMock.EXPECT().GetTeamProjects(gomock.Any(), teamID).Return(projects, nil).Times(3)163164firstPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{165TeamId: teamID,166Pagination: &v1.Pagination{167PageSize: 2,168},169}))170require.NoError(t, err)171requireEqualProto(t, &v1.ListProjectsResponse{172Projects: projectsToAPIResponse(projects[0:2]),173TotalResults: int32(len(projects)),174}, firstPage.Msg)175176secondPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{177TeamId: teamID,178Pagination: &v1.Pagination{179PageSize: 2,180Page: 2,181},182}))183require.NoError(t, err)184requireEqualProto(t, &v1.ListProjectsResponse{185Projects: projectsToAPIResponse(projects[2:4]),186TotalResults: int32(len(projects)),187}, secondPage.Msg)188189thirdPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{190TeamId: teamID,191Pagination: &v1.Pagination{192PageSize: 2,193Page: 3,194},195}))196require.NoError(t, err)197requireEqualProto(t, &v1.ListProjectsResponse{198Projects: projectsToAPIResponse(projects[4:]),199TotalResults: int32(len(projects)),200}, thirdPage.Msg)201})202203t.Run("retrieves projects for team, when team ID is specified and paginates", func(t *testing.T) {204serverMock, client := setupProjectsService(t)205206teamID := uuid.New().String()207208projects := []*protocol.Project{209newProject(&protocol.Project{}),210newProject(&protocol.Project{}),211newProject(&protocol.Project{}),212newProject(&protocol.Project{}),213newProject(&protocol.Project{}),214}215216serverMock.EXPECT().GetTeamProjects(gomock.Any(), teamID).Return(projects, nil).Times(3)217218firstPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{219TeamId: teamID,220Pagination: &v1.Pagination{221PageSize: 2,222},223}))224require.NoError(t, err)225requireEqualProto(t, &v1.ListProjectsResponse{226Projects: projectsToAPIResponse(projects[0:2]),227TotalResults: int32(len(projects)),228}, firstPage.Msg)229230secondPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{231TeamId: teamID,232Pagination: &v1.Pagination{233PageSize: 2,234Page: 2,235},236}))237require.NoError(t, err)238requireEqualProto(t, &v1.ListProjectsResponse{239Projects: projectsToAPIResponse(projects[2:4]),240TotalResults: int32(len(projects)),241}, secondPage.Msg)242243thirdPage, err := client.ListProjects(context.Background(), connect.NewRequest(&v1.ListProjectsRequest{244TeamId: teamID,245Pagination: &v1.Pagination{246PageSize: 2,247Page: 3,248},249}))250require.NoError(t, err)251requireEqualProto(t, &v1.ListProjectsResponse{252Projects: projectsToAPIResponse(projects[4:]),253TotalResults: int32(len(projects)),254}, thirdPage.Msg)255})256}257258func TestProjectsService_DeleteProject(t *testing.T) {259260t.Run("invalid argument when project ID is empty", func(t *testing.T) {261_, client := setupProjectsService(t)262263_, err := client.DeleteProject(context.Background(), connect.NewRequest(&v1.DeleteProjectRequest{264ProjectId: "",265}))266require.Error(t, err)267require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))268})269270t.Run("invalid argument when project ID is not a valid uuid", func(t *testing.T) {271_, client := setupProjectsService(t)272273_, err := client.DeleteProject(context.Background(), connect.NewRequest(&v1.DeleteProjectRequest{274ProjectId: "something",275}))276require.Error(t, err)277require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))278})279280t.Run("proxies to server", func(t *testing.T) {281serverMock, client := setupProjectsService(t)282283projectID := uuid.New().String()284285serverMock.EXPECT().DeleteProject(gomock.Any(), projectID).Return(nil)286287resp, err := client.DeleteProject(context.Background(), connect.NewRequest(&v1.DeleteProjectRequest{288ProjectId: projectID,289}))290require.NoError(t, err)291requireEqualProto(t, &v1.DeleteProjectResponse{}, resp.Msg)292})293}294295func setupProjectsService(t *testing.T) (*protocol.MockAPIInterface, v1connect.ProjectsServiceClient) {296t.Helper()297298ctrl := gomock.NewController(t)299t.Cleanup(ctrl.Finish)300301serverMock := protocol.NewMockAPIInterface(ctrl)302303svc := NewProjectsService(&FakeServerConnPool{304api: serverMock,305})306307keyset := jwstest.GenerateKeySet(t)308rsa256, err := jws.NewRSA256(keyset)309require.NoError(t, err)310311_, handler := v1connect.NewProjectsServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor(config.SessionConfig{312Issuer: "unitetest.com",313Cookie: config.CookieConfig{314Name: "cookie_jwt",315},316}, rsa256)))317318srv := httptest.NewServer(handler)319t.Cleanup(srv.Close)320321client := v1connect.NewProjectsServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(322auth.NewClientInterceptor("auth-token"),323))324325return serverMock, client326}327328func newProject(p *protocol.Project) *protocol.Project {329r := rand.Int()330b_false := false331result := &protocol.Project{332ID: uuid.New().String(),333Name: fmt.Sprintf("team-%d", r),334TeamID: uuid.New().String(),335CloneURL: "https://github.com/easyCZ/foobar",336AppInstallationID: "1337",337Settings: &protocol.ProjectSettings{338UsePersistentVolumeClaim: true,339WorkspaceClasses: &protocol.WorkspaceClassesSettings{340Regular: "default",341Prebuild: "default",342},343RestrictedWorkspaceClasses: &[]string{"default"},344PrebuildSettings: &protocol.PrebuildSettings{345Enable: &b_false,346},347},348CreationTime: "2022-09-09T09:09:09.000Z",349}350351if p.ID != "" {352result.ID = p.ID353}354if p.Name != "" {355result.Name = p.Name356}357if p.UserID != "" {358result.UserID = p.UserID359}360if p.TeamID != "" {361result.TeamID = p.TeamID362}363if p.CloneURL != "" {364result.CloneURL = p.CloneURL365}366if p.AppInstallationID != "" {367result.AppInstallationID = p.AppInstallationID368}369if p.Settings != nil {370result.Settings = p.Settings371}372373return result374}375376377