From 339af373359fbe4d92d27e50d35b50717bf01216 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Thu, 30 Mar 2023 23:09:14 +0200 Subject: [PATCH 1/7] Implement API endpoint to link a package to a repo Closes #21062. --- routers/api/v1/api.go | 1 + routers/api/v1/packages/package.go | 49 ++++++++++++++++++++++++++++++ services/packages/packages.go | 34 +++++++++++++++++++++ templates/swagger/v1_json.tmpl | 47 ++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 8d7669762b722..063b0a435bb82 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1412,6 +1412,7 @@ func Routes() *web.Route { m.Delete("", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) m.Get("/files", reqToken(), packages.ListPackageFiles) }) + m.Post("/{type}/{name}/link", reqPackageAccess(perm.AccessModeWrite), packages.LinkPackage) m.Get("/", reqToken(), packages.ListPackages) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead)) diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index a79ba315be2fc..49e2bb1f31220 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -213,3 +213,52 @@ func ListPackageFiles(ctx *context.APIContext) { ctx.JSON(http.StatusOK, apiPackageFiles) } + +// LinkPackage sets a repository link for a package +func LinkPackage(ctx *context.APIContext) { + // swagger:operation POST /packages/{owner}/{type}/{name}/link package linkPackage + // --- + // summary: Link a package to a repository + // parameters: + // - name: owner + // in: path + // description: owner of the package + // type: string + // required: true + // - name: type + // in: path + // description: type of the package + // type: string + // required: true + // - name: name + // in: path + // description: name of the package + // type: string + // required: true + // - name: repo + // in: query + // description: ID of the repository to link + // type: integer + // required: true + // responses: + // "201": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + + repoID := ctx.FormInt64("repo") + packageType := ctx.Params("type") + name := ctx.Params("name") + pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(packageType), name) + if err != nil { + ctx.Error(http.StatusInternalServerError, "LinkPackage", err) + return + } + + err = packages_service.LinkPackage(ctx, ctx.Doer, pkg, repoID) + if err != nil { + ctx.Error(http.StatusInternalServerError, "LinkPackage", err) + return + } + ctx.Status(http.StatusNoContent) +} diff --git a/services/packages/packages.go b/services/packages/packages.go index 56d5cc04de2df..d0cdc4471b5ce 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -14,7 +14,9 @@ import ( "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" + access_model "code.gitea.io/gitea/models/perm/access" repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" @@ -657,3 +659,35 @@ func RemoveAllPackages(ctx context.Context, userID int64) (int, error) { } return count, nil } + +func LinkPackage(ctx context.Context, doer *user_model.User, p *packages_model.Package, repoID int64) error { + if repoID != 0 { + repo, err := repo_model.GetRepositoryByID(ctx, repoID) + if err != nil { + return fmt.Errorf("Error getting repository %d: %w", repoID, err) + } + + canWrite := repo.OwnerID == doer.ID + + if !canWrite { + perms, err := access_model.GetUserRepoPermission(ctx, repo, doer) + if err != nil { + return fmt.Errorf("Error getting repository permissions for %d on %d: %w", doer.ID, repo.ID, err) + } + + canWrite = perms.CanWrite(unit.TypePackages) + } + + if !canWrite { + return fmt.Errorf("No permissions to link this package and repository") + } + + repoID = repo.ID + } + + if err := packages_model.SetRepositoryLink(ctx, p.ID, repoID); err != nil { + return fmt.Errorf("Error updating package: %w", err) + } + + return nil +} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 094a0e9aec9f9..4e4cc00fd7cd5 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2813,6 +2813,53 @@ } } }, + "/packages/{owner}/{type}/{name}/link": { + "post": { + "tags": [ + "package" + ], + "summary": "Link a package to a repository", + "operationId": "linkPackage", + "parameters": [ + { + "type": "string", + "description": "owner of the package", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "type of the package", + "name": "type", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the package", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "ID of the repository to link", + "name": "repo", + "in": "query", + "required": true + } + ], + "responses": { + "201": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/packages/{owner}/{type}/{name}/{version}": { "get": { "produces": [ From 84a2bc3dbb65f730498f31a127b0d1201957c3ec Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Fri, 31 Mar 2023 14:33:53 +0200 Subject: [PATCH 2/7] Add integration test for package linking API To satisfy the API's restrictions on write access when linking packages, the test needs to be run with a different user. --- routers/api/v1/api.go | 2 +- tests/integration/api_packages_test.go | 38 ++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 063b0a435bb82..9cb162d70b5e8 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1412,7 +1412,7 @@ func Routes() *web.Route { m.Delete("", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) m.Get("/files", reqToken(), packages.ListPackageFiles) }) - m.Post("/{type}/{name}/link", reqPackageAccess(perm.AccessModeWrite), packages.LinkPackage) + m.Post("/{type}/{name}/link", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.LinkPackage) m.Get("/", reqToken(), packages.ListPackages) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead)) diff --git a/tests/integration/api_packages_test.go b/tests/integration/api_packages_test.go index 8c981566b6028..7e50edcad5296 100644 --- a/tests/integration/api_packages_test.go +++ b/tests/integration/api_packages_test.go @@ -15,6 +15,7 @@ import ( "code.gitea.io/gitea/models/db" packages_model "code.gitea.io/gitea/models/packages" container_model "code.gitea.io/gitea/models/packages/container" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/setting" @@ -31,9 +32,17 @@ import ( func TestPackageAPI(t *testing.T) { defer tests.PrepareTestEnv(t)() - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) + repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID}) session := loginUser(t, user.Name) tokenReadPackage := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeReadPackage) + tokenWritePackage := getTokenForLoggedInUser( + t, + session, + auth_model.AccessTokenScopeWritePackage, + auth_model.AccessTokenScopeReadPackage, + auth_model.AccessTokenScopeWriteRepository, + ) tokenDeletePackage := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWritePackage) packageName := "test-package" @@ -99,7 +108,8 @@ func TestPackageAPI(t *testing.T) { assert.Nil(t, ap1.Repository) // link to public repository - assert.NoError(t, packages_model.SetRepositoryLink(db.DefaultContext, p.ID, 1)) + req = NewRequestf(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/link?repo=%d", user.Name, packageName, repo.ID)).AddTokenAuth(tokenWritePackage) + MakeRequest(t, req, http.StatusNoContent) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)). AddTokenAuth(tokenReadPackage) @@ -108,10 +118,15 @@ func TestPackageAPI(t *testing.T) { var ap2 *api.Package DecodeJSON(t, resp, &ap2) assert.NotNil(t, ap2.Repository) - assert.EqualValues(t, 1, ap2.Repository.ID) + assert.EqualValues(t, repo.ID, ap2.Repository.ID) - // link to private repository - assert.NoError(t, packages_model.SetRepositoryLink(db.DefaultContext, p.ID, 2)) + // link to repository without write access, should fail + req = NewRequestf(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/link?repo=%d", user.Name, packageName, 3)).AddTokenAuth(tokenWritePackage) + MakeRequest(t, req, http.StatusInternalServerError) + + // remove link + req = NewRequestf(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/link?repo=%d", user.Name, packageName, 0)).AddTokenAuth(tokenWritePackage) + MakeRequest(t, req, http.StatusNoContent) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)). AddTokenAuth(tokenReadPackage) @@ -121,7 +136,18 @@ func TestPackageAPI(t *testing.T) { DecodeJSON(t, resp, &ap3) assert.Nil(t, ap3.Repository) - assert.NoError(t, packages_model.UnlinkRepositoryFromAllPackages(db.DefaultContext, 2)) + // force link to a repository the currently logged-in user doesn't have access to + privateRepoID := int64(6) + assert.NoError(t, packages_model.SetRepositoryLink(db.DefaultContext, p.ID, privateRepoID)) + + req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).AddTokenAuth(tokenReadPackage) + resp = MakeRequest(t, req, http.StatusOK) + + var ap4 *api.Package + DecodeJSON(t, resp, &ap4) + assert.Nil(t, ap4.Repository) + + assert.NoError(t, packages_model.UnlinkRepositoryFromAllPackages(db.DefaultContext, privateRepoID)) }) }) From 10df3c4659bd64f7238adc8751f9cc088cb2bbc7 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Sun, 2 Apr 2023 14:12:13 +0200 Subject: [PATCH 3/7] Rename function Co-authored-by: KN4CK3R --- routers/api/v1/packages/package.go | 2 +- services/packages/packages.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 49e2bb1f31220..92e39c2f1c3a5 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -255,7 +255,7 @@ func LinkPackage(ctx *context.APIContext) { return } - err = packages_service.LinkPackage(ctx, ctx.Doer, pkg, repoID) + err = packages_service.LinkPackageToRepository(ctx, ctx.Doer, pkg, repoID) if err != nil { ctx.Error(http.StatusInternalServerError, "LinkPackage", err) return diff --git a/services/packages/packages.go b/services/packages/packages.go index d0cdc4471b5ce..6fb3949cf0792 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -660,7 +660,7 @@ func RemoveAllPackages(ctx context.Context, userID int64) (int, error) { return count, nil } -func LinkPackage(ctx context.Context, doer *user_model.User, p *packages_model.Package, repoID int64) error { +func LinkPackageToRepository(ctx context.Context, doer *user_model.User, p *packages_model.Package, repoID int64) error { if repoID != 0 { repo, err := repo_model.GetRepositoryByID(ctx, repoID) if err != nil { From 6e2661b433b3a1656480eb26a4895f9dacbf9157 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Tue, 2 May 2023 14:40:57 +0200 Subject: [PATCH 4/7] Improve error reporting Co-authored-by: KN4CK3R --- routers/api/v1/packages/package.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 92e39c2f1c3a5..2452ae28c9f49 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -4,6 +4,7 @@ package packages import ( + "errors" "net/http" "code.gitea.io/gitea/models/packages" @@ -246,19 +247,21 @@ func LinkPackage(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - repoID := ctx.FormInt64("repo") - packageType := ctx.Params("type") - name := ctx.Params("name") - pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(packageType), name) + pkg, err := packages.GetPackageByName(ctx, ctx.ContextUser.ID, packages.Type(ctx.Params("type")), ctx.Params("name")) if err != nil { - ctx.Error(http.StatusInternalServerError, "LinkPackage", err) + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetPackageByName", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetPackageByName", err) + } return } - err = packages_service.LinkPackageToRepository(ctx, ctx.Doer, pkg, repoID) + err = packages_service.LinkPackageToRepository(ctx, ctx.Doer, pkg, ctx.FormInt64("repo")) if err != nil { - ctx.Error(http.StatusInternalServerError, "LinkPackage", err) + ctx.Error(http.StatusInternalServerError, "LinkPackageToRepository", err) return } + ctx.Status(http.StatusNoContent) } From f07cfa943276bb762fac7e85c576a1b859695733 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Wed, 17 Jan 2024 09:34:16 +0100 Subject: [PATCH 5/7] Fix package repository link permission check Co-authored-by: Denys Konovalov --- services/packages/packages.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/services/packages/packages.go b/services/packages/packages.go index 6fb3949cf0792..9cd662cd7c7b5 100644 --- a/services/packages/packages.go +++ b/services/packages/packages.go @@ -667,22 +667,16 @@ func LinkPackageToRepository(ctx context.Context, doer *user_model.User, p *pack return fmt.Errorf("Error getting repository %d: %w", repoID, err) } - canWrite := repo.OwnerID == doer.ID - - if !canWrite { - perms, err := access_model.GetUserRepoPermission(ctx, repo, doer) - if err != nil { - return fmt.Errorf("Error getting repository permissions for %d on %d: %w", doer.ID, repo.ID, err) - } - - canWrite = perms.CanWrite(unit.TypePackages) + perms, err := access_model.GetUserRepoPermission(ctx, repo, doer) + if err != nil { + return fmt.Errorf("Error getting permissions for user %d on repository %d: %w", doer.ID, repo.ID, err) } + canWrite := perms.CanWrite(unit.TypePackages) + if !canWrite { - return fmt.Errorf("No permissions to link this package and repository") + return fmt.Errorf("No permission to link this package and repository, or packages are disabled") } - - repoID = repo.ID } if err := packages_model.SetRepositoryLink(ctx, p.ID, repoID); err != nil { From 9fb1f1cefea996daeed35244f74ec5600e5a62fe Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 22 Jan 2024 09:37:56 +0100 Subject: [PATCH 6/7] Refactor router group Co-authored-by: KN4CK3R --- routers/api/v1/api.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 9cb162d70b5e8..81da8a50cd120 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1407,12 +1407,14 @@ func Routes() *web.Route { // NOTE: these are Gitea package management API - see packages.CommonRoutes and packages.DockerContainerRoutes for endpoints that implement package manager APIs m.Group("/packages/{username}", func() { - m.Group("/{type}/{name}/{version}", func() { - m.Get("", reqToken(), packages.GetPackage) - m.Delete("", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) - m.Get("/files", reqToken(), packages.ListPackageFiles) - }) - m.Post("/{type}/{name}/link", reqToken(), reqPackageAccess(perm.AccessModeWrite), packages.LinkPackage) + m.Group("/{type}/{name}", func() { + m.Group("/{version}", func() { + m.Get("", packages.GetPackage) + m.Delete("", reqPackageAccess(perm.AccessModeWrite), packages.DeletePackage) + m.Get("/files", packages.ListPackageFiles) + }) + m.Post("/link", reqPackageAccess(perm.AccessModeWrite), packages.LinkPackage) + }, reqToken()) m.Get("/", reqToken(), packages.ListPackages) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead)) From 221dcdc4beb7bf79bcd1ad060172800aa28d28a9 Mon Sep 17 00:00:00 2001 From: Lucas Schwiderski Date: Mon, 22 Jan 2024 10:56:38 +0100 Subject: [PATCH 7/7] Allow {owner}/{name} repo format when linking package --- routers/api/v1/packages/package.go | 54 ++++++++++++++++++++++++-- templates/swagger/v1_json.tmpl | 13 +++++-- tests/integration/api_packages_test.go | 6 +-- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/routers/api/v1/packages/package.go b/routers/api/v1/packages/package.go index 2452ae28c9f49..3952378691e3a 100644 --- a/routers/api/v1/packages/package.go +++ b/routers/api/v1/packages/package.go @@ -5,9 +5,13 @@ package packages import ( "errors" + "fmt" "net/http" + "strconv" + "strings" "code.gitea.io/gitea/models/packages" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/modules/context" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/util" @@ -236,11 +240,16 @@ func LinkPackage(ctx *context.APIContext) { // description: name of the package // type: string // required: true - // - name: repo + // - name: repo_id // in: query - // description: ID of the repository to link + // description: ID of the repository to link. Pass `0` to unlink // type: integer - // required: true + // required: false + // - name: repo_name + // in: query + // description: name of the repository to link, format `{owner}/{name}` + // type: string + // required: false // responses: // "201": // "$ref": "#/responses/empty" @@ -257,7 +266,44 @@ func LinkPackage(ctx *context.APIContext) { return } - err = packages_service.LinkPackageToRepository(ctx, ctx.Doer, pkg, ctx.FormInt64("repo")) + var repoID int64 + + formRepoID := ctx.FormString("repo_id") + repoName := ctx.FormString("repo_name") + + if len(formRepoID) == 0 && len(repoName) == 0 { + ctx.Error(http.StatusBadRequest, "Missing parameter", fmt.Errorf("Either `repo_id` or `repo_name` must be given")) + return + } + + if len(formRepoID) > 0 { + repoID, err = strconv.ParseInt(ctx.FormString("repo_id"), 10, 64) + if err != nil { + ctx.Error(http.StatusBadRequest, "Invalid parameter", fmt.Errorf("`repo_id` must be a valid integer, if given")) + return + } + } else { + nameParts := strings.Split(repoName, "/") + if len(nameParts) != 2 { + ctx.Error(http.StatusBadRequest, "Invalid parameter", fmt.Errorf("`repo_name` must be of format `{owner}/{name}`, if given")) + return + } + + repo, err := repo_model.GetRepositoryByOwnerAndName(ctx, nameParts[0], nameParts[1]) + if err != nil { + if errors.Is(err, util.ErrNotExist) { + ctx.Error(http.StatusNotFound, "GetRepositoryByOwnerAndName", err) + } else { + ctx.Error(http.StatusInternalServerError, "GetRepositoryByOwnerAndName", err) + } + + return + } + + repoID = repo.ID + } + + err = packages_service.LinkPackageToRepository(ctx, ctx.Doer, pkg, repoID) if err != nil { ctx.Error(http.StatusInternalServerError, "LinkPackageToRepository", err) return diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 4e4cc00fd7cd5..6c39721f4196b 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2844,10 +2844,15 @@ }, { "type": "integer", - "description": "ID of the repository to link", - "name": "repo", - "in": "query", - "required": true + "description": "ID of the repository to link. Pass `0` to unlink", + "name": "repo_id", + "in": "query" + }, + { + "type": "string", + "description": "name of the repository to link, format `{owner}/{name}`", + "name": "repo_name", + "in": "query" } ], "responses": { diff --git a/tests/integration/api_packages_test.go b/tests/integration/api_packages_test.go index 7e50edcad5296..94d84ddc748bb 100644 --- a/tests/integration/api_packages_test.go +++ b/tests/integration/api_packages_test.go @@ -108,7 +108,7 @@ func TestPackageAPI(t *testing.T) { assert.Nil(t, ap1.Repository) // link to public repository - req = NewRequestf(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/link?repo=%d", user.Name, packageName, repo.ID)).AddTokenAuth(tokenWritePackage) + req = NewRequestf(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/link?repo_name=%s/%s", user.Name, packageName, repo.OwnerName, repo.Name)).AddTokenAuth(tokenWritePackage) MakeRequest(t, req, http.StatusNoContent) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)). @@ -121,11 +121,11 @@ func TestPackageAPI(t *testing.T) { assert.EqualValues(t, repo.ID, ap2.Repository.ID) // link to repository without write access, should fail - req = NewRequestf(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/link?repo=%d", user.Name, packageName, 3)).AddTokenAuth(tokenWritePackage) + req = NewRequestf(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/link?repo_id=%d", user.Name, packageName, 3)).AddTokenAuth(tokenWritePackage) MakeRequest(t, req, http.StatusInternalServerError) // remove link - req = NewRequestf(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/link?repo=%d", user.Name, packageName, 0)).AddTokenAuth(tokenWritePackage) + req = NewRequestf(t, "POST", fmt.Sprintf("/api/v1/packages/%s/generic/%s/link?repo_id=%d", user.Name, packageName, 0)).AddTokenAuth(tokenWritePackage) MakeRequest(t, req, http.StatusNoContent) req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s", user.Name, packageName, packageVersion)).