diff --git a/internal/services/gateway/action/projectfavorite.go b/internal/services/gateway/action/projectfavorite.go new file mode 100644 index 000000000..aff0d620d --- /dev/null +++ b/internal/services/gateway/action/projectfavorite.go @@ -0,0 +1,119 @@ +// Copyright 2019 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package action + +import ( + "context" + + "github.com/sorintlab/errors" + + "agola.io/agola/internal/services/gateway/common" + "agola.io/agola/internal/util" + csapitypes "agola.io/agola/services/configstore/api/types" + "agola.io/agola/services/configstore/client" + cstypes "agola.io/agola/services/configstore/types" +) + +type CreateUserProjectFavoriteRequest struct { + ProjectRef string +} + +func (h *ActionHandler) CreateUserProjectFavorite(ctx context.Context, req *CreateUserProjectFavoriteRequest) (*cstypes.ProjectFavorite, error) { + if !common.IsUserLogged(ctx) { + return nil, errors.Errorf("user not logged in") + } + + userID := common.CurrentUserID(ctx) + + creq := &csapitypes.CreateProjectFavoriteRequest{ + UserRef: userID, + ProjectRef: req.ProjectRef, + } + + projectFavorite, _, err := h.configstoreClient.CreateProjectFavorite(ctx, creq) + if err != nil { + return nil, util.NewAPIError(util.KindFromRemoteError(err), errors.Wrapf(err, "failed to create project favorite")) + } + + return projectFavorite, nil +} + +func (h *ActionHandler) DeleteUserProjectFavorite(ctx context.Context, projectRef string) error { + if !common.IsUserLogged(ctx) { + return errors.Errorf("user not logged in") + } + + userID := common.CurrentUserID(ctx) + + if _, err := h.configstoreClient.DeleteProjectFavorite(ctx, userID, projectRef); err != nil { + return util.NewAPIError(util.KindFromRemoteError(err), errors.Wrapf(err, "failed to delete project favorite")) + } + return nil +} + +type GetUserProjectFavoritesRequest struct { + Cursor string + + Limit int + SortDirection SortDirection +} + +type GetUserProjectFavoritesResponse struct { + ProjectFavorites []*cstypes.ProjectFavorite + Cursor string +} + +func (h *ActionHandler) GetUserProjectFavorites(ctx context.Context, req *GetUserProjectFavoritesRequest) (*GetUserProjectFavoritesResponse, error) { + if !common.IsUserLogged(ctx) { + return nil, errors.Errorf("user not logged in") + } + userID := common.CurrentUserID(ctx) + + inCursor := &StartCursor{} + sortDirection := req.SortDirection + if req.Cursor != "" { + if err := UnmarshalCursor(req.Cursor, inCursor); err != nil { + return nil, errors.WithStack(err) + } + sortDirection = inCursor.SortDirection + } + if sortDirection == "" { + sortDirection = SortDirectionAsc + } + + projectFavorites, resp, err := h.configstoreClient.GetProjectFavorites(ctx, userID, &client.GetProjectFavoritesOptions{ListOptions: &client.ListOptions{Limit: req.Limit, SortDirection: cstypes.SortDirection(sortDirection)}, StartProjectFavoriteID: inCursor.Start}) + if err != nil { + return nil, util.NewAPIError(util.KindFromRemoteError(err), err) + } + + var outCursor string + if resp.HasMore && len(projectFavorites) > 0 { + lastProjectFavoriteID := projectFavorites[len(projectFavorites)-1].ID + outCursor, err = MarshalCursor(&StartCursor{ + Start: lastProjectFavoriteID, + SortDirection: sortDirection, + }) + if err != nil { + return nil, errors.WithStack(err) + } + } + + res := &GetUserProjectFavoritesResponse{ + ProjectFavorites: projectFavorites, + Cursor: outCursor, + } + + return res, nil +} diff --git a/internal/services/gateway/api/projectfavorite.go b/internal/services/gateway/api/projectfavorite.go new file mode 100644 index 000000000..4f9e06ecd --- /dev/null +++ b/internal/services/gateway/api/projectfavorite.go @@ -0,0 +1,136 @@ +// Copyright 2024 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/rs/zerolog" + "github.com/sorintlab/errors" + + "agola.io/agola/internal/services/gateway/action" + util "agola.io/agola/internal/util" + cstypes "agola.io/agola/services/configstore/types" + gwapitypes "agola.io/agola/services/gateway/api/types" +) + +type CreateUserProjectFavoriteHandler struct { + log zerolog.Logger + ah *action.ActionHandler +} + +func NewCreateUserProjectFavoriteHandler(log zerolog.Logger, ah *action.ActionHandler) *CreateUserProjectFavoriteHandler { + return &CreateUserProjectFavoriteHandler{log: log, ah: ah} +} + +func (h *CreateUserProjectFavoriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + + projectRef := vars["projectref"] + + creq := &action.CreateUserProjectFavoriteRequest{ + ProjectRef: projectRef, + } + + projectFavorite, err := h.ah.CreateUserProjectFavorite(ctx, creq) + if util.HTTPError(w, err) { + h.log.Err(err).Send() + return + } + + if err := util.HTTPResponse(w, http.StatusCreated, projectFavorite); err != nil { + h.log.Err(err).Send() + } +} + +type DeleteUserProjectFavoriteHandler struct { + log zerolog.Logger + ah *action.ActionHandler +} + +func NewDeleteUserProjectFavoriteHandler(log zerolog.Logger, ah *action.ActionHandler) *DeleteUserProjectFavoriteHandler { + return &DeleteUserProjectFavoriteHandler{log: log, ah: ah} +} + +func (h *DeleteUserProjectFavoriteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + vars := mux.Vars(r) + + projectRef := vars["projectref"] + + err := h.ah.DeleteUserProjectFavorite(ctx, projectRef) + if util.HTTPError(w, err) { + h.log.Err(err).Send() + return + } + + if err := util.HTTPResponse(w, http.StatusNoContent, nil); err != nil { + h.log.Err(err).Send() + } +} + +type UserProjectFavoritesHandler struct { + log zerolog.Logger + ah *action.ActionHandler +} + +func NewUserProjectFavoritesHandler(log zerolog.Logger, ah *action.ActionHandler) *UserProjectFavoritesHandler { + return &UserProjectFavoritesHandler{log: log, ah: ah} +} + +func (h *UserProjectFavoritesHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + res, err := h.do(w, r) + if util.HTTPError(w, err) { + h.log.Err(err).Send() + return + } + + if err := util.HTTPResponse(w, http.StatusOK, res); err != nil { + h.log.Err(err).Send() + } +} + +func (h *UserProjectFavoritesHandler) do(w http.ResponseWriter, r *http.Request) ([]*gwapitypes.ProjectFavoriteResponse, error) { + ctx := r.Context() + + ropts, err := parseRequestOptions(r) + if err != nil { + return nil, errors.WithStack(err) + } + + ares, err := h.ah.GetUserProjectFavorites(ctx, &action.GetUserProjectFavoritesRequest{Cursor: ropts.Cursor, Limit: ropts.Limit, SortDirection: action.SortDirection(ropts.SortDirection)}) + if err != nil { + return nil, errors.WithStack(err) + } + + projectFavorites := make([]*gwapitypes.ProjectFavoriteResponse, len(ares.ProjectFavorites)) + for i, p := range ares.ProjectFavorites { + projectFavorites[i] = createProjectFavoriteResponse(p) + } + + addCursorHeader(w, ares.Cursor) + + return projectFavorites, nil +} + +func createProjectFavoriteResponse(o *cstypes.ProjectFavorite) *gwapitypes.ProjectFavoriteResponse { + org := &gwapitypes.ProjectFavoriteResponse{ + ID: o.ID, + ProjectID: o.ProjectID, + } + return org +} diff --git a/internal/services/gateway/gateway.go b/internal/services/gateway/gateway.go index 26f29c8af..ee4f5856c 100644 --- a/internal/services/gateway/gateway.go +++ b/internal/services/gateway/gateway.go @@ -265,6 +265,10 @@ func (g *Gateway) Run(ctx context.Context) error { userRunLogsHandler := api.NewLogsHandler(g.log, g.ah, scommon.GroupTypeUser) userRunLogsDeleteHandler := api.NewLogsDeleteHandler(g.log, g.ah, scommon.GroupTypeUser) + createUserProjectFavoriteHandler := api.NewCreateUserProjectFavoriteHandler(g.log, g.ah) + deleteUserProjectFavoriteHandler := api.NewDeleteUserProjectFavoriteHandler(g.log, g.ah) + userProjectFavoritesHandler := api.NewUserProjectFavoritesHandler(g.log, g.ah) + userRemoteReposHandler := api.NewUserRemoteReposHandler(g.log, g.ah, g.configstoreClient) badgeHandler := api.NewBadgeHandler(g.log, g.ah) @@ -354,6 +358,9 @@ func (g *Gateway) Run(ctx context.Context) error { apirouter.Handle("/user/orgs", authForcedHandler(userOrgsHandler)).Methods("GET") apirouter.Handle("/user/org_invitations", authForcedHandler(userOrgInvitationsHandler)).Methods("GET") apirouter.Handle("/user/org_invitations/{orgref}/actions", authForcedHandler(userOrgInvitationActionHandler)).Methods("PUT") + apirouter.Handle("/user/projects/{projectref}/projectfavorites", authForcedHandler(createUserProjectFavoriteHandler)).Methods("POST") + apirouter.Handle("/user/projects/{projectref}/projectfavorites", authForcedHandler(deleteUserProjectFavoriteHandler)).Methods("DELETE") + apirouter.Handle("/user/projectfavorites", authForcedHandler(userProjectFavoritesHandler)).Methods("GET") apirouter.Handle("/users/{userref}/runs", authForcedHandler(userRunsHandler)).Methods("GET") apirouter.Handle("/users/{userref}/runs/{runnumber}", authOptionalHandler(userRunHandler)).Methods("GET") diff --git a/services/configstore/api/types/projectfavorite.go b/services/configstore/api/types/projectfavorite.go new file mode 100644 index 000000000..8b967bc24 --- /dev/null +++ b/services/configstore/api/types/projectfavorite.go @@ -0,0 +1,20 @@ +// Copyright 2024 Sorint.lab +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +type CreateProjectFavoriteRequest struct { + UserRef string + ProjectRef string +} diff --git a/services/configstore/client/client.go b/services/configstore/client/client.go index 12be508bf..081de9bc6 100644 --- a/services/configstore/client/client.go +++ b/services/configstore/client/client.go @@ -778,3 +778,42 @@ func (c *Client) Import(ctx context.Context, r io.Reader) (*Response, error) { resp, err := c.GetResponse(ctx, "POST", "/import", nil, -1, common.JSONContent, r) return resp, errors.WithStack(err) } + +func (c *Client) CreateProjectFavorite(ctx context.Context, req *csapitypes.CreateProjectFavoriteRequest) (*cstypes.ProjectFavorite, *Response, error) { + reqj, err := json.Marshal(req) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + projectFavorite := new(cstypes.ProjectFavorite) + resp, err := c.GetParsedResponse(ctx, "POST", fmt.Sprintf("/users/%s/projects/%s/projectfavorites", req.UserRef, req.ProjectRef), nil, common.JSONContent, bytes.NewReader(reqj), projectFavorite) + return projectFavorite, resp, errors.WithStack(err) +} + +func (c *Client) DeleteProjectFavorite(ctx context.Context, userRef string, projectRef string) (*Response, error) { + resp, err := c.GetResponse(ctx, "DELETE", fmt.Sprintf("/users/%s/projects/%s/projectfavorites", userRef, projectRef), nil, -1, common.JSONContent, nil) + return resp, errors.WithStack(err) +} + +type GetProjectFavoritesOptions struct { + *ListOptions + + StartProjectFavoriteID string +} + +func (o *GetProjectFavoritesOptions) Add(q url.Values) { + o.ListOptions.Add(q) + + if o.StartProjectFavoriteID != "" { + q.Add("startprojectfavoriteid", o.StartProjectFavoriteID) + } +} + +func (c *Client) GetProjectFavorites(ctx context.Context, userRef string, opts *GetProjectFavoritesOptions) ([]*cstypes.ProjectFavorite, *Response, error) { + q := url.Values{} + opts.Add(q) + + projectFavorites := []*cstypes.ProjectFavorite{} + resp, err := c.GetParsedResponse(ctx, "GET", fmt.Sprintf("/users/%s/projectfavorites", userRef), q, common.JSONContent, nil, &projectFavorites) + return projectFavorites, resp, errors.WithStack(err) +} diff --git a/services/gateway/api/types/projectfavorite.go b/services/gateway/api/types/projectfavorite.go new file mode 100644 index 000000000..ce2ab503c --- /dev/null +++ b/services/gateway/api/types/projectfavorite.go @@ -0,0 +1,10 @@ +package types + +type ProjectFavoriteResponse struct { + ID string `json:"id"` + ProjectID string `json:"project_id"` +} + +type CreateProjectFavoriteRequest struct { + ProjectRef string +} diff --git a/services/gateway/client/client.go b/services/gateway/client/client.go index 6127ea6a7..33ef8d49f 100644 --- a/services/gateway/client/client.go +++ b/services/gateway/client/client.go @@ -871,3 +871,27 @@ func (c *Client) GetProjectCommitStatusDeliveries(ctx context.Context, projectRe func (c *Client) ProjectCommitStatusRedelivery(ctx context.Context, projectRef string, commitStatusDeliveryID string) (*Response, error) { return c.getResponse(ctx, "PUT", fmt.Sprintf("/projects/%s/commitstatusdeliveries/%s/redelivery", projectRef, commitStatusDeliveryID), nil, jsonContent, nil) } + +func (c *Client) GetUserProjectFavorites(ctx context.Context, opts *ListOptions) ([]*gwapitypes.ProjectFavoriteResponse, *Response, error) { + q := url.Values{} + opts.Add(q) + + projectFavorites := []*gwapitypes.ProjectFavoriteResponse{} + resp, err := c.getParsedResponse(ctx, "GET", "/user/projectfavorites", q, jsonContent, nil, &projectFavorites) + return projectFavorites, resp, errors.WithStack(err) +} + +func (c *Client) CreateUserProjectFavorite(ctx context.Context, req *gwapitypes.CreateProjectFavoriteRequest) (*gwapitypes.ProjectFavoriteResponse, *Response, error) { + reqj, err := json.Marshal(req) + if err != nil { + return nil, nil, errors.WithStack(err) + } + + projectFavorite := new(gwapitypes.ProjectFavoriteResponse) + resp, err := c.getParsedResponse(ctx, "POST", fmt.Sprintf("/user/projects/%s/projectfavorites", req.ProjectRef), nil, jsonContent, bytes.NewReader(reqj), projectFavorite) + return projectFavorite, resp, errors.WithStack(err) +} + +func (c *Client) DeleteUserProjectFavorite(ctx context.Context, projectRef string) (*Response, error) { + return c.getResponse(ctx, "DELETE", fmt.Sprintf("/user/projects/%s/projectfavorites", projectRef), nil, jsonContent, nil) +} diff --git a/tests/api_test.go b/tests/api_test.go index c5337fe87..50df91338 100644 --- a/tests/api_test.go +++ b/tests/api_test.go @@ -8,6 +8,7 @@ import ( "os" "path" "path/filepath" + "sort" "strconv" "testing" "time" @@ -3553,3 +3554,329 @@ func testGetGroupRuns(t *testing.T, userRun bool) { }) } } + +type projectFavoritesByID []*gwapitypes.ProjectFavoriteResponse + +func (p projectFavoritesByID) Len() int { return len(p) } +func (p projectFavoritesByID) Less(i, j int) bool { + return p[i].ID < p[j].ID +} +func (p projectFavoritesByID) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +func TestProjectFavorite(t *testing.T) { + t.Parallel() + + type testProjectFavoriteConfig struct { + sc *setupContext + tokenUser01 string + tokenUser02 string + gwAdminClient *gwclient.Client + gwClientUser01 *gwclient.Client + gwClientUser02 *gwclient.Client + projects []*gwapitypes.ProjectResponse + } + + tests := []struct { + name string + f func(ctx context.Context, t *testing.T, tc *testProjectFavoriteConfig) + }{ + { + name: "create user project favorite", + f: func(ctx context.Context, t *testing.T, tc *testProjectFavoriteConfig) { + var expectedUser01ProjectFavorites []*gwapitypes.ProjectFavoriteResponse + var expectedUser02ProjectFavorites []*gwapitypes.ProjectFavoriteResponse + + for i := 0; i < 2; i++ { + projectFavorite, _, err := tc.gwClientUser01.CreateUserProjectFavorite(ctx, &gwapitypes.CreateProjectFavoriteRequest{ProjectRef: tc.projects[i].ID}) + testutil.NilError(t, err) + + expectedUser01ProjectFavorites = append(expectedUser01ProjectFavorites, projectFavorite) + } + sort.Sort(projectFavoritesByID(expectedUser01ProjectFavorites)) + + for i := 2; i < 4; i++ { + projectFavorite, _, err := tc.gwClientUser02.CreateUserProjectFavorite(ctx, &gwapitypes.CreateProjectFavoriteRequest{ProjectRef: tc.projects[i].ID}) + testutil.NilError(t, err) + + expectedUser02ProjectFavorites = append(expectedUser02ProjectFavorites, projectFavorite) + } + sort.Sort(projectFavoritesByID(expectedUser02ProjectFavorites)) + + projectFavorites, _, err := tc.gwClientUser01.GetUserProjectFavorites(ctx, nil) + testutil.NilError(t, err) + assert.DeepEqual(t, projectFavorites, expectedUser01ProjectFavorites) + + projectFavorites, _, err = tc.gwClientUser02.GetUserProjectFavorites(ctx, nil) + testutil.NilError(t, err) + assert.DeepEqual(t, projectFavorites, expectedUser02ProjectFavorites) + }, + }, + { + name: "test project favorite creation with already existing project favorite", + f: func(ctx context.Context, t *testing.T, tc *testProjectFavoriteConfig) { + projectFavorite, _, err := tc.gwClientUser01.CreateUserProjectFavorite(ctx, &gwapitypes.CreateProjectFavoriteRequest{ProjectRef: tc.projects[0].ID}) + testutil.NilError(t, err) + + _, _, err = tc.gwClientUser01.CreateUserProjectFavorite(ctx, &gwapitypes.CreateProjectFavoriteRequest{ProjectRef: tc.projects[0].ID}) + expectedErr := remoteErrorBadRequest + assert.Error(t, err, expectedErr) + + projectFavorites, _, err := tc.gwClientUser01.GetUserProjectFavorites(ctx, nil) + testutil.NilError(t, err) + assert.Assert(t, cmp.Equal(len(projectFavorites), 1)) + assert.DeepEqual(t, projectFavorites[0], projectFavorite) + }, + }, + { + name: "test project favorite creation with not existing project", + f: func(ctx context.Context, t *testing.T, tc *testProjectFavoriteConfig) { + _, _, err := tc.gwClientUser01.CreateUserProjectFavorite(ctx, &gwapitypes.CreateProjectFavoriteRequest{ProjectRef: "projecttest"}) + expectedErr := remoteErrorBadRequest + assert.Error(t, err, expectedErr) + + projectFavorites, _, err := tc.gwClientUser01.GetUserProjectFavorites(ctx, nil) + testutil.NilError(t, err) + assert.Assert(t, cmp.Equal(len(projectFavorites), 0)) + }, + }, + { + name: "test project favorite deletion", + f: func(ctx context.Context, t *testing.T, tc *testProjectFavoriteConfig) { + _, _, err := tc.gwClientUser01.CreateUserProjectFavorite(ctx, &gwapitypes.CreateProjectFavoriteRequest{ProjectRef: tc.projects[0].ID}) + testutil.NilError(t, err) + + _, err = tc.gwClientUser01.DeleteUserProjectFavorite(ctx, tc.projects[0].ID) + testutil.NilError(t, err) + + projectFavorites, _, err := tc.gwClientUser01.GetUserProjectFavorites(ctx, nil) + testutil.NilError(t, err) + assert.Assert(t, cmp.Equal(len(projectFavorites), 0)) + }, + }, + { + name: "test project favorite deletion with not existing project favorite", + f: func(ctx context.Context, t *testing.T, tc *testProjectFavoriteConfig) { + _, err := tc.gwClientUser01.DeleteUserProjectFavorite(ctx, tc.projects[0].ID) + expectedErr := remoteErrorBadRequest + assert.Error(t, err, expectedErr) + }, + }, + { + name: "test project deletion with existing project favorite", + f: func(ctx context.Context, t *testing.T, tc *testProjectFavoriteConfig) { + _, _, err := tc.gwClientUser01.CreateUserProjectFavorite(ctx, &gwapitypes.CreateProjectFavoriteRequest{ProjectRef: tc.projects[0].ID}) + testutil.NilError(t, err) + + _, err = tc.gwClientUser01.DeleteProject(ctx, tc.projects[0].ID) + testutil.NilError(t, err) + + projectFavorites, _, err := tc.gwClientUser01.GetUserProjectFavorites(ctx, nil) + testutil.NilError(t, err) + assert.Assert(t, cmp.Equal(len(projectFavorites), 0)) + }, + }, + { + name: "test get project preferites with admin user", + f: func(ctx context.Context, t *testing.T, tc *testProjectFavoriteConfig) { + _, _, err := tc.gwAdminClient.GetUserProjectFavorites(ctx, nil) + expectedErr := remoteErrorInternal + assert.Error(t, err, expectedErr) + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sc := setup(ctx, t, dir, withGitea(true)) + defer sc.stop() + + gwAdminClient := gwclient.NewClient(sc.config.Gateway.APIExposedURL, sc.config.Gateway.AdminToken) + + giteaToken, tokenUser01 := createLinkedAccount(ctx, t, sc.gitea, sc.config) + gwClientUser01 := gwclient.NewClient(sc.config.Gateway.APIExposedURL, tokenUser01) + + _, _, err := gwAdminClient.CreateUser(ctx, &gwapitypes.CreateUserRequest{UserName: agolaUser02}) + testutil.NilError(t, err) + + tokenUser02, _, err := gwAdminClient.CreateUserToken(ctx, agolaUser02, &gwapitypes.CreateUserTokenRequest{TokenName: "test"}) + testutil.NilError(t, err) + + gwClientUser02 := gwclient.NewClient(sc.config.Gateway.APIExposedURL, tokenUser02.Token) + + _, _, err = gwClientUser01.CreateOrg(ctx, &gwapitypes.CreateOrgRequest{Name: agolaOrg01, Visibility: gwapitypes.VisibilityPublic}) + testutil.NilError(t, err) + + giteaAPIURL := fmt.Sprintf("http://%s:%s", sc.gitea.HTTPListenAddress, sc.gitea.HTTPPort) + + giteaClient, err := gitea.NewClient(giteaAPIURL, gitea.SetToken(giteaToken)) + testutil.NilError(t, err) + + var projects []*gwapitypes.ProjectResponse + + _, project := createProject(ctx, t, giteaClient, gwClientUser01, withVisibility(gwapitypes.VisibilityPrivate)) + projects = append(projects, project) + + for i := 2; i < 5; i++ { + project, _, err = gwClientUser01.CreateProject(ctx, &gwapitypes.CreateProjectRequest{ + Name: fmt.Sprintf("project0%d", i), + ParentRef: path.Join("org", agolaOrg01), + RemoteSourceName: "gitea", + RepoPath: path.Join(giteaUser01, "repo01"), + Visibility: gwapitypes.VisibilityPublic, + }) + testutil.NilError(t, err) + + projects = append(projects, project) + } + + tc := &testProjectFavoriteConfig{ + sc: sc, + tokenUser01: tokenUser01, + tokenUser02: tokenUser02.Token, + gwClientUser01: gwClientUser01, + gwClientUser02: gwClientUser02, + gwAdminClient: gwAdminClient, + projects: projects, + } + + tt.f(ctx, t, tc) + }) + } +} + +func TestGetProjectFavorites(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sc := setup(ctx, t, dir, withGitea(true)) + defer sc.stop() + + giteaToken, tokenUser01 := createLinkedAccount(ctx, t, sc.gitea, sc.config) + gwClientUser01 := gwclient.NewClient(sc.config.Gateway.APIExposedURL, tokenUser01) + + giteaAPIURL := fmt.Sprintf("http://%s:%s", sc.gitea.HTTPListenAddress, sc.gitea.HTTPPort) + + giteaClient, err := gitea.NewClient(giteaAPIURL, gitea.SetToken(giteaToken)) + testutil.NilError(t, err) + + allProjectFavorites := []*gwapitypes.ProjectFavoriteResponse{} + + _, project := createProject(ctx, t, giteaClient, gwClientUser01, withVisibility(gwapitypes.VisibilityPrivate)) + + projectFavorite, _, err := gwClientUser01.CreateUserProjectFavorite(ctx, &gwapitypes.CreateProjectFavoriteRequest{ProjectRef: project.ID}) + testutil.NilError(t, err) + allProjectFavorites = append(allProjectFavorites, projectFavorite) + + for i := 2; i < 10; i++ { + project, _, err := gwClientUser01.CreateProject(ctx, &gwapitypes.CreateProjectRequest{ + Name: fmt.Sprintf("project0%d", i), + ParentRef: path.Join("user", agolaUser01), + RemoteSourceName: "gitea", + RepoPath: path.Join(giteaUser01, "repo01"), + Visibility: gwapitypes.VisibilityPublic, + }) + testutil.NilError(t, err) + + projectFavorite, _, err := gwClientUser01.CreateUserProjectFavorite(ctx, &gwapitypes.CreateProjectFavoriteRequest{ProjectRef: project.ID}) + testutil.NilError(t, err) + + allProjectFavorites = append(allProjectFavorites, projectFavorite) + } + sort.Sort(projectFavoritesByID(allProjectFavorites)) + + tests := []struct { + name string + limit int + sortDirection gwapitypes.SortDirection + expectedCallsNumber int + }{ + { + name: "get user project stars with limit = 0, no sortdirection", + expectedCallsNumber: 1, + }, + { + name: "get user project stars with limit = 0", + sortDirection: gwapitypes.SortDirectionAsc, + expectedCallsNumber: 1, + }, + { + name: "get user project stars with limit less than results length", + limit: 2, + sortDirection: gwapitypes.SortDirectionAsc, + expectedCallsNumber: 5, + }, + { + name: "get user project stars with limit greater than results length", + limit: MaxLimit, + sortDirection: gwapitypes.SortDirectionAsc, + expectedCallsNumber: 1, + }, + { + name: "get user project stars with limit = 0, sortDirection desc", + sortDirection: gwapitypes.SortDirectionDesc, + expectedCallsNumber: 1, + }, + { + name: "get user project stars with limit less than results length, sortDirection desc", + limit: 2, + sortDirection: gwapitypes.SortDirectionDesc, + expectedCallsNumber: 5, + }, + { + name: "get user project stars with limit greater than results length, sortDirection desc", + limit: MaxLimit, + sortDirection: gwapitypes.SortDirectionDesc, + expectedCallsNumber: 1, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + var expectedProjectFavorites []*gwapitypes.ProjectFavoriteResponse + expectedProjectFavorites = append(expectedProjectFavorites, allProjectFavorites...) + + // reverse if sortDirection is desc + // TODO(sgotti) use go 1.21 generics slices.Reverse when removing support for go < 1.21 + if tt.sortDirection == gwapitypes.SortDirectionDesc { + for i, j := 0, len(expectedProjectFavorites)-1; i < j; i, j = i+1, j-1 { + expectedProjectFavorites[i], expectedProjectFavorites[j] = expectedProjectFavorites[j], expectedProjectFavorites[i] + } + } + + respAllProjectFavorites := []*gwapitypes.ProjectFavoriteResponse{} + sortDirection := tt.sortDirection + callsNumber := 0 + var cursor string + + for { + respProjectFavorites, res, err := gwClientUser01.GetUserProjectFavorites(ctx, &gwclient.ListOptions{Cursor: cursor, Limit: tt.limit, SortDirection: sortDirection}) + testutil.NilError(t, err) + + callsNumber++ + + respAllProjectFavorites = append(respAllProjectFavorites, respProjectFavorites...) + + if res.Cursor == "" { + break + } + cursor = res.Cursor + sortDirection = "" + } + + assert.DeepEqual(t, expectedProjectFavorites, respAllProjectFavorites) + assert.Assert(t, cmp.Equal(callsNumber, tt.expectedCallsNumber)) + }) + } +}