aboutsummaryrefslogtreecommitdiffstats
path: root/integrations
diff options
context:
space:
mode:
authorKN4CK3R <admin@oldschoolhack.me>2022-03-30 10:42:47 +0200
committerGitHub <noreply@github.com>2022-03-30 16:42:47 +0800
commit1d332342db6d5bd4e1552d8d46720bf1b948c26b (patch)
treeca0c8931e5da85e71037ed43d7a90826ba708d9d /integrations
parent2bce1ea9862c70ebb69963e65bb84dcad6ebb31c (diff)
downloadgitea-1d332342db6d5bd4e1552d8d46720bf1b948c26b.tar.gz
gitea-1d332342db6d5bd4e1552d8d46720bf1b948c26b.zip
Add Package Registry (#16510)
* Added package store settings. * Added models. * Added generic package registry. * Added tests. * Added NuGet package registry. * Moved service index to api file. * Added NPM package registry. * Added Maven package registry. * Added PyPI package registry. * Summary is deprecated. * Changed npm name. * Sanitize project url. * Allow only scoped packages. * Added user interface. * Changed method name. * Added missing migration file. * Set page info. * Added documentation. * Added documentation links. * Fixed wrong error message. * Lint template files. * Fixed merge errors. * Fixed unit test storage path. * Switch to json module. * Added suggestions. * Added package webhook. * Add package api. * Fixed swagger file. * Fixed enum and comments. * Fixed NuGet pagination. * Print test names. * Added api tests. * Fixed access level. * Fix User unmarshal. * Added RubyGems package registry. * Fix lint. * Implemented io.Writer. * Added support for sha256/sha512 checksum files. * Improved maven-metadata.xml support. * Added support for symbol package uploads. * Added tests. * Added overview docs. * Added npm dependencies and keywords. * Added no-packages information. * Display file size. * Display asset count. * Fixed filter alignment. * Added package icons. * Formatted instructions. * Allow anonymous package downloads. * Fixed comments. * Fixed postgres test. * Moved file. * Moved models to models/packages. * Use correct error response format per client. * Use simpler search form. * Fixed IsProd. * Restructured data model. * Prevent empty filename. * Fix swagger. * Implemented user/org registry. * Implemented UI. * Use GetUserByIDCtx. * Use table for dependencies. * make svg * Added support for unscoped npm packages. * Add support for npm dist tags. * Added tests for npm tags. * Unlink packages if repository gets deleted. * Prevent user/org delete if a packages exist. * Use package unlink in repository service. * Added support for composer packages. * Restructured package docs. * Added missing tests. * Fixed generic content page. * Fixed docs. * Fixed swagger. * Added missing type. * Fixed ambiguous column. * Organize content store by sha256 hash. * Added admin package management. * Added support for sorting. * Add support for multiple identical versions/files. * Added missing repository unlink. * Added file properties. * make fmt * lint * Added Conan package registry. * Updated docs. * Unify package names. * Added swagger enum. * Use longer TEXT column type. * Removed version composite key. * Merged package and container registry. * Removed index. * Use dedicated package router. * Moved files to new location. * Updated docs. * Fixed JOIN order. * Fixed GROUP BY statement. * Fixed GROUP BY #2. * Added symbol server support. * Added more tests. * Set NOT NULL. * Added setting to disable package registries. * Moved auth into service. * refactor * Use ctx everywhere. * Added package cleanup task. * Changed packages path. * Added container registry. * Refactoring * Updated comparison. * Fix swagger. * Fixed table order. * Use token auth for npm routes. * Enabled ReverseProxy auth. * Added packages link for orgs. * Fixed anonymous org access. * Enable copy button for setup instructions. * Merge error * Added suggestions. * Fixed merge. * Handle "generic". * Added link for TODO. * Added suggestions. * Changed temporary buffer filename. * Added suggestions. * Apply suggestions from code review Co-authored-by: Thomas Boerger <thomas@webhippie.de> * Update docs/content/doc/packages/nuget.en-us.md Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: Thomas Boerger <thomas@webhippie.de>
Diffstat (limited to 'integrations')
-rw-r--r--integrations/api_packages_composer_test.go214
-rw-r--r--integrations/api_packages_conan_test.go724
-rw-r--r--integrations/api_packages_container_test.go534
-rw-r--r--integrations/api_packages_generic_test.go109
-rw-r--r--integrations/api_packages_maven_test.go205
-rw-r--r--integrations/api_packages_npm_test.go222
-rw-r--r--integrations/api_packages_nuget_test.go381
-rw-r--r--integrations/api_packages_pypi_test.go181
-rw-r--r--integrations/api_packages_rubygems_test.go226
-rw-r--r--integrations/api_packages_test.go102
10 files changed, 2898 insertions, 0 deletions
diff --git a/integrations/api_packages_composer_test.go b/integrations/api_packages_composer_test.go
new file mode 100644
index 0000000000..59b975408d
--- /dev/null
+++ b/integrations/api_packages_composer_test.go
@@ -0,0 +1,214 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+ "archive/zip"
+ "bytes"
+ "fmt"
+ "net/http"
+ neturl "net/url"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ composer_module "code.gitea.io/gitea/modules/packages/composer"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/routers/api/packages/composer"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageComposer(t *testing.T) {
+ defer prepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+
+ vendorName := "gitea"
+ projectName := "composer-package"
+ packageName := vendorName + "/" + projectName
+ packageVersion := "1.0.3"
+ packageDescription := "Package Description"
+ packageType := "composer-plugin"
+ packageAuthor := "Gitea Authors"
+ packageLicense := "MIT"
+
+ var buf bytes.Buffer
+ archive := zip.NewWriter(&buf)
+ w, _ := archive.Create("composer.json")
+ w.Write([]byte(`{
+ "name": "` + packageName + `",
+ "description": "` + packageDescription + `",
+ "type": "` + packageType + `",
+ "license": "` + packageLicense + `",
+ "authors": [
+ {
+ "name": "` + packageAuthor + `"
+ }
+ ]
+ }`))
+ archive.Close()
+ content := buf.Bytes()
+
+ url := fmt.Sprintf("%sapi/packages/%s/composer", setting.AppURL, user.Name)
+
+ t.Run("ServiceIndex", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/packages.json", url))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result composer.ServiceIndexResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, url+"/search.json?q=%query%&type=%type%", result.SearchTemplate)
+ assert.Equal(t, url+"/p2/%package%.json", result.MetadataTemplate)
+ assert.Equal(t, url+"/list.json", result.PackageList)
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ t.Run("MissingVersion", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusBadRequest)
+ })
+
+ t.Run("Valid", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ uploadURL := url + "?version=" + packageVersion
+
+ req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeComposer)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &composer_module.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, fmt.Sprintf("%s-%s.%s.zip", vendorName, projectName, packageVersion), pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), pb.Size)
+
+ req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusBadRequest)
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeComposer)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(0), pvs[0].DownloadCount)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/files/%s/%s/%s", url, neturl.PathEscape(packageName), neturl.PathEscape(pvs[0].LowerVersion), neturl.PathEscape(pfs[0].LowerName)))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, content, resp.Body.Bytes())
+
+ pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeComposer)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(1), pvs[0].DownloadCount)
+ })
+
+ t.Run("SearchService", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ cases := []struct {
+ Query string
+ Type string
+ Page int
+ PerPage int
+ ExpectedTotal int64
+ ExpectedResults int
+ }{
+ {"", "", 0, 0, 1, 1},
+ {"", "", 1, 1, 1, 1},
+ {"test", "", 1, 0, 0, 0},
+ {"gitea", "", 1, 1, 1, 1},
+ {"gitea", "", 2, 1, 1, 0},
+ {"", packageType, 1, 1, 1, 1},
+ {"gitea", packageType, 1, 1, 1, 1},
+ {"gitea", "dummy", 1, 1, 0, 0},
+ }
+
+ for i, c := range cases {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/search.json?q=%s&type=%s&page=%d&per_page=%d", url, c.Query, c.Type, c.Page, c.PerPage))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result composer.SearchResultResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, c.ExpectedTotal, result.Total, "case %d: unexpected total hits", i)
+ assert.Len(t, result.Results, c.ExpectedResults, "case %d: unexpected result count", i)
+ }
+ })
+
+ t.Run("EnumeratePackages", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", url+"/list.json")
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result map[string][]string
+ DecodeJSON(t, resp, &result)
+
+ assert.Contains(t, result, "packageNames")
+ names := result["packageNames"]
+ assert.Len(t, names, 1)
+ assert.Equal(t, packageName, names[0])
+ })
+
+ t.Run("PackageMetadata", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/p2/%s/%s.json", url, vendorName, projectName))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result composer.PackageMetadataResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Contains(t, result.Packages, packageName)
+ pkgs := result.Packages[packageName]
+ assert.Len(t, pkgs, 1)
+ assert.Equal(t, packageName, pkgs[0].Name)
+ assert.Equal(t, packageVersion, pkgs[0].Version)
+ assert.Equal(t, packageType, pkgs[0].Type)
+ assert.Equal(t, packageDescription, pkgs[0].Description)
+ assert.Len(t, pkgs[0].Authors, 1)
+ assert.Equal(t, packageAuthor, pkgs[0].Authors[0].Name)
+ assert.Equal(t, "zip", pkgs[0].Dist.Type)
+ assert.Equal(t, "7b40bfd6da811b2b78deec1e944f156dbb2c747b", pkgs[0].Dist.Checksum)
+ })
+}
diff --git a/integrations/api_packages_conan_test.go b/integrations/api_packages_conan_test.go
new file mode 100644
index 0000000000..65d16801fc
--- /dev/null
+++ b/integrations/api_packages_conan_test.go
@@ -0,0 +1,724 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+ "fmt"
+ "net/http"
+ stdurl "net/url"
+ "strings"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ conan_model "code.gitea.io/gitea/models/packages/conan"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ conan_module "code.gitea.io/gitea/modules/packages/conan"
+ "code.gitea.io/gitea/modules/setting"
+ conan_router "code.gitea.io/gitea/routers/api/packages/conan"
+
+ "github.com/stretchr/testify/assert"
+)
+
+const (
+ conanfileName = "conanfile.py"
+ conaninfoName = "conaninfo.txt"
+
+ conanLicense = "MIT"
+ conanAuthor = "Gitea <info@gitea.io>"
+ conanHomepage = "https://gitea.io/"
+ conanURL = "https://gitea.com/"
+ conanDescription = "Description of ConanPackage"
+ conanTopic = "gitea"
+
+ conanPackageReference = "dummyreference"
+
+ contentConaninfo = `[settings]
+ arch=x84_64
+
+[requires]
+ fmt/7.1.3
+
+[options]
+ shared=False
+
+[full_settings]
+ arch=x84_64
+
+[full_requires]
+ fmt/7.1.3
+
+[full_options]
+ shared=False
+
+[recipe_hash]
+ 74714915a51073acb548ca1ce29afbac
+
+[env]
+CC=gcc-10`
+)
+
+func addTokenAuthHeader(request *http.Request, token string) *http.Request {
+ request.Header.Set("Authorization", token)
+ return request
+}
+
+func buildConanfileContent(name, version string) string {
+ return `from conans import ConanFile, CMake, tools
+
+class ConanPackageConan(ConanFile):
+ name = "` + name + `"
+ version = "` + version + `"
+ license = "` + conanLicense + `"
+ author = "` + conanAuthor + `"
+ homepage = "` + conanHomepage + `"
+ url = "` + conanURL + `"
+ description = "` + conanDescription + `"
+ topics = ("` + conanTopic + `")
+ settings = "os", "compiler", "build_type", "arch"
+ options = {"shared": [True, False], "fPIC": [True, False]}
+ default_options = {"shared": False, "fPIC": True}
+ generators = "cmake"`
+}
+
+func uploadConanPackageV1(t *testing.T, baseURL, token, name, version, user, channel string) {
+ contentConanfile := buildConanfileContent(name, version)
+
+ recipeURL := fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s", baseURL, name, version, user, channel)
+
+ req := NewRequest(t, "GET", recipeURL)
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/digest", recipeURL))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/download_urls", recipeURL))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "POST", fmt.Sprintf("%s/upload_urls", recipeURL))
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("%s/upload_urls", recipeURL), map[string]int64{
+ conanfileName: int64(len(contentConanfile)),
+ "removed.txt": 0,
+ })
+ req = addTokenAuthHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ uploadURLs := make(map[string]string)
+ DecodeJSON(t, resp, &uploadURLs)
+
+ assert.Contains(t, uploadURLs, conanfileName)
+ assert.NotContains(t, uploadURLs, "removed.txt")
+
+ uploadURL := uploadURLs[conanfileName]
+ assert.NotEmpty(t, uploadURL)
+
+ req = NewRequestWithBody(t, "PUT", uploadURL, strings.NewReader(contentConanfile))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ packageURL := fmt.Sprintf("%s/packages/%s", recipeURL, conanPackageReference)
+
+ req = NewRequest(t, "GET", packageURL)
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/digest", packageURL))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/download_urls", packageURL))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "POST", fmt.Sprintf("%s/upload_urls", packageURL))
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequestWithJSON(t, "POST", fmt.Sprintf("%s/upload_urls", packageURL), map[string]int64{
+ conaninfoName: int64(len(contentConaninfo)),
+ "removed.txt": 0,
+ })
+ req = addTokenAuthHeader(req, token)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ uploadURLs = make(map[string]string)
+ DecodeJSON(t, resp, &uploadURLs)
+
+ assert.Contains(t, uploadURLs, conaninfoName)
+ assert.NotContains(t, uploadURLs, "removed.txt")
+
+ uploadURL = uploadURLs[conaninfoName]
+ assert.NotEmpty(t, uploadURL)
+
+ req = NewRequestWithBody(t, "PUT", uploadURL, strings.NewReader(contentConaninfo))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusCreated)
+}
+
+func uploadConanPackageV2(t *testing.T, baseURL, token, name, version, user, channel, recipeRevision, packageRevision string) {
+ contentConanfile := buildConanfileContent(name, version)
+
+ recipeURL := fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s", baseURL, name, version, user, channel, recipeRevision)
+
+ req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/files/%s", recipeURL, conanfileName), strings.NewReader(contentConanfile))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/files", recipeURL))
+ req = addTokenAuthHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var list *struct {
+ Files map[string]interface{} `json:"files"`
+ }
+ DecodeJSON(t, resp, &list)
+ assert.Len(t, list.Files, 1)
+ assert.Contains(t, list.Files, conanfileName)
+
+ packageURL := fmt.Sprintf("%s/packages/%s/revisions/%s", recipeURL, conanPackageReference, packageRevision)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/files", packageURL))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/files/%s", packageURL, conaninfoName), strings.NewReader(contentConaninfo))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/files", packageURL))
+ req = addTokenAuthHeader(req, token)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ list = nil
+ DecodeJSON(t, resp, &list)
+ assert.Len(t, list.Files, 1)
+ assert.Contains(t, list.Files, conaninfoName)
+}
+
+func TestPackageConan(t *testing.T) {
+ defer prepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+
+ name := "ConanPackage"
+ version1 := "1.2"
+ version2 := "1.3"
+ user1 := "dummy"
+ user2 := "gitea"
+ channel1 := "test"
+ channel2 := "final"
+ revision1 := "rev1"
+ revision2 := "rev2"
+
+ url := fmt.Sprintf("%sapi/packages/%s/conan", setting.AppURL, user.Name)
+
+ t.Run("v1", func(t *testing.T) {
+ t.Run("Ping", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/ping", url))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, "revisions", resp.Header().Get("X-Conan-Server-Capabilities"))
+ })
+
+ token := ""
+
+ t.Run("Authenticate", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/users/authenticate", url))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ body := resp.Body.String()
+ assert.NotEmpty(t, body)
+
+ token = fmt.Sprintf("Bearer %s", body)
+ })
+
+ t.Run("CheckCredentials", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/users/check_credentials", url))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ uploadConanPackageV1(t, url, token, name, version1, user1, channel1)
+
+ t.Run("Validate", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeConan)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.Equal(t, name, pd.Package.Name)
+ assert.Equal(t, version1, pd.Version.Version)
+ assert.IsType(t, &conan_module.Metadata{}, pd.Metadata)
+ metadata := pd.Metadata.(*conan_module.Metadata)
+ assert.Equal(t, conanLicense, metadata.License)
+ assert.Equal(t, conanAuthor, metadata.Author)
+ assert.Equal(t, conanHomepage, metadata.ProjectURL)
+ assert.Equal(t, conanURL, metadata.RepositoryURL)
+ assert.Equal(t, conanDescription, metadata.Description)
+ assert.Equal(t, []string{conanTopic}, metadata.Keywords)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 2)
+
+ for _, pf := range pfs {
+ pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
+ assert.NoError(t, err)
+
+ if pf.Name == conanfileName {
+ assert.True(t, pf.IsLead)
+
+ assert.Equal(t, int64(len(buildConanfileContent(name, version1))), pb.Size)
+ } else if pf.Name == conaninfoName {
+ assert.False(t, pf.IsLead)
+
+ assert.Equal(t, int64(len(contentConaninfo)), pb.Size)
+ } else {
+ assert.Fail(t, "unknown file: %s", pf.Name)
+ }
+ }
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ recipeURL := fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s", url, name, version1, user1, channel1)
+
+ req := NewRequest(t, "GET", recipeURL)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ fileHashes := make(map[string]string)
+ DecodeJSON(t, resp, &fileHashes)
+ assert.Len(t, fileHashes, 1)
+ assert.Contains(t, fileHashes, conanfileName)
+ assert.Equal(t, "7abc52241c22090782c54731371847a8", fileHashes[conanfileName])
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/digest", recipeURL))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ downloadURLs := make(map[string]string)
+ DecodeJSON(t, resp, &downloadURLs)
+ assert.Contains(t, downloadURLs, conanfileName)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/download_urls", recipeURL))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &downloadURLs)
+ assert.Contains(t, downloadURLs, conanfileName)
+
+ req = NewRequest(t, "GET", downloadURLs[conanfileName])
+ resp = MakeRequest(t, req, http.StatusOK)
+ assert.Equal(t, buildConanfileContent(name, version1), resp.Body.String())
+
+ packageURL := fmt.Sprintf("%s/packages/%s", recipeURL, conanPackageReference)
+
+ req = NewRequest(t, "GET", packageURL)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ fileHashes = make(map[string]string)
+ DecodeJSON(t, resp, &fileHashes)
+ assert.Len(t, fileHashes, 1)
+ assert.Contains(t, fileHashes, conaninfoName)
+ assert.Equal(t, "7628bfcc5b17f1470c468621a78df394", fileHashes[conaninfoName])
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/digest", packageURL))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ downloadURLs = make(map[string]string)
+ DecodeJSON(t, resp, &downloadURLs)
+ assert.Contains(t, downloadURLs, conaninfoName)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/download_urls", packageURL))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &downloadURLs)
+ assert.Contains(t, downloadURLs, conaninfoName)
+
+ req = NewRequest(t, "GET", downloadURLs[conaninfoName])
+ resp = MakeRequest(t, req, http.StatusOK)
+ assert.Equal(t, contentConaninfo, resp.Body.String())
+ })
+
+ t.Run("Search", func(t *testing.T) {
+ uploadConanPackageV1(t, url, token, name, version2, user1, channel1)
+ uploadConanPackageV1(t, url, token, name, version1, user1, channel2)
+ uploadConanPackageV1(t, url, token, name, version1, user2, channel1)
+ uploadConanPackageV1(t, url, token, name, version1, user2, channel2)
+
+ t.Run("Recipe", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ cases := []struct {
+ Query string
+ Expected []string
+ }{
+ {"ConanPackage", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.2", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.1", []string{}},
+ {"Conan*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/*2", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1*2", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.2@", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.2@du*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@dummy/final"}},
+ {"ConanPackage/1.2@du*/", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@dummy/final"}},
+ {"ConanPackage/1.2@du*/*test", []string{"ConanPackage/1.2@dummy/test"}},
+ {"ConanPackage/1.2@du*/*st", []string{"ConanPackage/1.2@dummy/test"}},
+ {"ConanPackage/1.2@gitea/*", []string{"ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"*/*@dummy", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@dummy/final"}},
+ {"*/*@*/final", []string{"ConanPackage/1.2@dummy/final", "ConanPackage/1.2@gitea/final"}},
+ }
+
+ for i, c := range cases {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/conans/search?q=%s", url, stdurl.QueryEscape(c.Query)))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result *conan_router.SearchResult
+ DecodeJSON(t, resp, &result)
+
+ assert.ElementsMatch(t, c.Expected, result.Results, "case %d: unexpected result", i)
+ }
+ })
+
+ t.Run("Package", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s/search", url, name, version1, user1, channel2))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result map[string]*conan_module.Conaninfo
+ DecodeJSON(t, resp, &result)
+
+ assert.Contains(t, result, conanPackageReference)
+ info := result[conanPackageReference]
+ assert.NotEmpty(t, info.Settings)
+ })
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ t.Run("Package", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ cases := []struct {
+ Channel string
+ References []string
+ }{
+ {channel1, []string{conanPackageReference}},
+ {channel2, []string{}},
+ }
+
+ for i, c := range cases {
+ rref, _ := conan_module.NewRecipeReference(name, version1, user1, c.Channel, conan_module.DefaultRevision)
+ references, err := conan_model.GetPackageReferences(db.DefaultContext, user.ID, rref)
+ assert.NoError(t, err)
+ assert.NotEmpty(t, references)
+
+ req := NewRequestWithJSON(t, "POST", fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s/packages/delete", url, name, version1, user1, c.Channel), map[string][]string{
+ "package_ids": c.References,
+ })
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ references, err = conan_model.GetPackageReferences(db.DefaultContext, user.ID, rref)
+ assert.NoError(t, err)
+ assert.Empty(t, references, "case %d: should be empty", i)
+ }
+ })
+
+ t.Run("Recipe", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ cases := []struct {
+ Channel string
+ }{
+ {channel1},
+ {channel2},
+ }
+
+ for i, c := range cases {
+ rref, _ := conan_module.NewRecipeReference(name, version1, user1, c.Channel, conan_module.DefaultRevision)
+ revisions, err := conan_model.GetRecipeRevisions(db.DefaultContext, user.ID, rref)
+ assert.NoError(t, err)
+ assert.NotEmpty(t, revisions)
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/v1/conans/%s/%s/%s/%s", url, name, version1, user1, c.Channel))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ revisions, err = conan_model.GetRecipeRevisions(db.DefaultContext, user.ID, rref)
+ assert.NoError(t, err)
+ assert.Empty(t, revisions, "case %d: should be empty", i)
+ }
+ })
+ })
+ })
+
+ t.Run("v2", func(t *testing.T) {
+ t.Run("Ping", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/ping", url))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, "revisions", resp.Header().Get("X-Conan-Server-Capabilities"))
+ })
+
+ token := ""
+
+ t.Run("Authenticate", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/users/authenticate", url))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ body := resp.Body.String()
+ assert.NotEmpty(t, body)
+
+ token = fmt.Sprintf("Bearer %s", body)
+ })
+
+ t.Run("CheckCredentials", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/users/check_credentials", url))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ uploadConanPackageV2(t, url, token, name, version1, user1, channel1, revision1, revision1)
+
+ t.Run("Validate", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeConan)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 2)
+ })
+ })
+
+ t.Run("Latest", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ recipeURL := fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s", url, name, version1, user1, channel1)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/latest", recipeURL))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ obj := make(map[string]string)
+ DecodeJSON(t, resp, &obj)
+ assert.Contains(t, obj, "revision")
+ assert.Equal(t, revision1, obj["revision"])
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/revisions/%s/packages/%s/latest", recipeURL, revision1, conanPackageReference))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ obj = make(map[string]string)
+ DecodeJSON(t, resp, &obj)
+ assert.Contains(t, obj, "revision")
+ assert.Equal(t, revision1, obj["revision"])
+ })
+
+ t.Run("ListRevisions", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ uploadConanPackageV2(t, url, token, name, version1, user1, channel1, revision1, revision2)
+ uploadConanPackageV2(t, url, token, name, version1, user1, channel1, revision2, revision1)
+ uploadConanPackageV2(t, url, token, name, version1, user1, channel1, revision2, revision2)
+
+ recipeURL := fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions", url, name, version1, user1, channel1)
+
+ req := NewRequest(t, "GET", recipeURL)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type RevisionInfo struct {
+ Revision string `json:"revision"`
+ Time time.Time `json:"time"`
+ }
+
+ type RevisionList struct {
+ Revisions []*RevisionInfo `json:"revisions"`
+ }
+
+ var list *RevisionList
+ DecodeJSON(t, resp, &list)
+ assert.Len(t, list.Revisions, 2)
+ revs := make([]string, 0, len(list.Revisions))
+ for _, rev := range list.Revisions {
+ revs = append(revs, rev.Revision)
+ }
+ assert.ElementsMatch(t, []string{revision1, revision2}, revs)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/packages/%s/revisions", recipeURL, revision1, conanPackageReference))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ DecodeJSON(t, resp, &list)
+ assert.Len(t, list.Revisions, 2)
+ revs = make([]string, 0, len(list.Revisions))
+ for _, rev := range list.Revisions {
+ revs = append(revs, rev.Revision)
+ }
+ assert.ElementsMatch(t, []string{revision1, revision2}, revs)
+ })
+
+ t.Run("Search", func(t *testing.T) {
+ t.Run("Recipe", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ cases := []struct {
+ Query string
+ Expected []string
+ }{
+ {"ConanPackage", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.2", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.1", []string{}},
+ {"Conan*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1*", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/*2", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1*2", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.2@", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"ConanPackage/1.2@du*", []string{"ConanPackage/1.2@dummy/test"}},
+ {"ConanPackage/1.2@du*/", []string{"ConanPackage/1.2@dummy/test"}},
+ {"ConanPackage/1.2@du*/*test", []string{"ConanPackage/1.2@dummy/test"}},
+ {"ConanPackage/1.2@du*/*st", []string{"ConanPackage/1.2@dummy/test"}},
+ {"ConanPackage/1.2@gitea/*", []string{"ConanPackage/1.2@gitea/test", "ConanPackage/1.2@gitea/final"}},
+ {"*/*@dummy", []string{"ConanPackage/1.2@dummy/test", "ConanPackage/1.3@dummy/test"}},
+ {"*/*@*/final", []string{"ConanPackage/1.2@gitea/final"}},
+ }
+
+ for i, c := range cases {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/conans/search?q=%s", url, stdurl.QueryEscape(c.Query)))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result *conan_router.SearchResult
+ DecodeJSON(t, resp, &result)
+
+ assert.ElementsMatch(t, c.Expected, result.Results, "case %d: unexpected result", i)
+ }
+ })
+
+ t.Run("Package", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/search", url, name, version1, user1, channel1))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result map[string]*conan_module.Conaninfo
+ DecodeJSON(t, resp, &result)
+
+ assert.Contains(t, result, conanPackageReference)
+ info := result[conanPackageReference]
+ assert.NotEmpty(t, info.Settings)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/search", url, name, version1, user1, channel1, revision1))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ result = make(map[string]*conan_module.Conaninfo)
+ DecodeJSON(t, resp, &result)
+
+ assert.Contains(t, result, conanPackageReference)
+ info = result[conanPackageReference]
+ assert.NotEmpty(t, info.Settings)
+ })
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ t.Run("Package", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ rref, _ := conan_module.NewRecipeReference(name, version1, user1, channel1, revision1)
+ pref, _ := conan_module.NewPackageReference(rref, conanPackageReference, conan_module.DefaultRevision)
+
+ checkPackageRevisionCount := func(count int) {
+ revisions, err := conan_model.GetPackageRevisions(db.DefaultContext, user.ID, pref)
+ assert.NoError(t, err)
+ assert.Len(t, revisions, count)
+ }
+ checkPackageReferenceCount := func(count int) {
+ references, err := conan_model.GetPackageReferences(db.DefaultContext, user.ID, rref)
+ assert.NoError(t, err)
+ assert.Len(t, references, count)
+ }
+
+ checkPackageRevisionCount(2)
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/packages/%s/revisions/%s", url, name, version1, user1, channel1, revision1, conanPackageReference, revision1))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkPackageRevisionCount(1)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/packages/%s", url, name, version1, user1, channel1, revision1, conanPackageReference))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkPackageRevisionCount(0)
+
+ rref = rref.WithRevision(revision2)
+
+ checkPackageReferenceCount(1)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s/packages", url, name, version1, user1, channel1, revision2))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkPackageReferenceCount(0)
+ })
+
+ t.Run("Recipe", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ rref, _ := conan_module.NewRecipeReference(name, version1, user1, channel1, conan_module.DefaultRevision)
+
+ checkRecipeRevisionCount := func(count int) {
+ revisions, err := conan_model.GetRecipeRevisions(db.DefaultContext, user.ID, rref)
+ assert.NoError(t, err)
+ assert.Len(t, revisions, count)
+ }
+
+ checkRecipeRevisionCount(2)
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s/revisions/%s", url, name, version1, user1, channel1, revision1))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkRecipeRevisionCount(1)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("%s/v2/conans/%s/%s/%s/%s", url, name, version1, user1, channel1))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkRecipeRevisionCount(0)
+ })
+ })
+ })
+}
diff --git a/integrations/api_packages_container_test.go b/integrations/api_packages_container_test.go
new file mode 100644
index 0000000000..a8f49423e2
--- /dev/null
+++ b/integrations/api_packages_container_test.go
@@ -0,0 +1,534 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ packages_model "code.gitea.io/gitea/models/packages"
+ container_model "code.gitea.io/gitea/models/packages/container"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ container_module "code.gitea.io/gitea/modules/packages/container"
+ "code.gitea.io/gitea/modules/packages/container/oci"
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageContainer(t *testing.T) {
+ defer prepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+
+ has := func(l packages_model.PackagePropertyList, name string) bool {
+ for _, pp := range l {
+ if pp.Name == name {
+ return true
+ }
+ }
+ return false
+ }
+
+ images := []string{"test", "te/st"}
+ tags := []string{"latest", "main"}
+ multiTag := "multi"
+
+ unknownDigest := "sha256:0000000000000000000000000000000000000000000000000000000000000000"
+
+ blobDigest := "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
+ blobContent, _ := base64.StdEncoding.DecodeString(`H4sIAAAJbogA/2IYBaNgFIxYAAgAAP//Lq+17wAEAAA=`)
+
+ configDigest := "sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d"
+ configContent := `{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/true"],"ArgsEscaped":true,"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"container":"b89fe92a887d55c0961f02bdfbfd8ac3ddf66167db374770d2d9e9fab3311510","container_config":{"Hostname":"b89fe92a887d","Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD [\"/true\"]"],"ArgsEscaped":true,"Image":"sha256:9bd8b88dc68b80cffe126cc820e4b52c6e558eb3b37680bfee8e5f3ed7b8c257"},"created":"2022-01-01T00:00:00.000000000Z","docker_version":"20.10.12","history":[{"created":"2022-01-01T00:00:00.000000000Z","created_by":"/bin/sh -c #(nop) COPY file:0e7589b0c800daaf6fa460d2677101e4676dd9491980210cb345480e513f3602 in /true "},{"created":"2022-01-01T00:00:00.000000001Z","created_by":"/bin/sh -c #(nop) CMD [\"/true\"]","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:0ff3b91bdf21ecdf2f2f3d4372c2098a14dbe06cd678e8f0a85fd4902d00e2e2"]}}`
+
+ manifestDigest := "sha256:4f10484d1c1bb13e3956b4de1cd42db8e0f14a75be1617b60f2de3cd59c803c6"
+ manifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeDockerManifest + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}`
+
+ untaggedManifestDigest := "sha256:4305f5f5572b9a426b88909b036e52ee3cf3d7b9c1b01fac840e90747f56623d"
+ untaggedManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageManifest + `","config":{"mediaType":"application/vnd.docker.container.image.v1+json","digest":"sha256:4607e093bec406eaadb6f3a340f63400c9d3a7038680744c406903766b938f0d","size":1069},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","digest":"sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4","size":32}]}`
+
+ indexManifestDigest := "sha256:bab112d6efb9e7f221995caaaa880352feb5bd8b1faf52fae8d12c113aa123ec"
+ indexManifestContent := `{"schemaVersion":2,"mediaType":"` + oci.MediaTypeImageIndex + `","manifests":[{"mediaType":"` + oci.MediaTypeDockerManifest + `","digest":"` + manifestDigest + `","platform":{"os":"linux","architecture":"arm","variant":"v7"}},{"mediaType":"` + oci.MediaTypeImageManifest + `","digest":"` + untaggedManifestDigest + `","platform":{"os":"linux","architecture":"arm64","variant":"v8"}}]}`
+
+ anonymousToken := ""
+ userToken := ""
+
+ t.Run("Authenticate", func(t *testing.T) {
+ type TokenResponse struct {
+ Token string `json:"token"`
+ }
+
+ authenticate := []string{
+ `Bearer realm="` + setting.AppURL + `v2/token"`,
+ `Basic`,
+ }
+
+ t.Run("Anonymous", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
+ resp := MakeRequest(t, req, http.StatusUnauthorized)
+
+ assert.ElementsMatch(t, authenticate, resp.Header().Values("WWW-Authenticate"))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL))
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ tokenResponse := &TokenResponse{}
+ DecodeJSON(t, resp, &tokenResponse)
+
+ assert.NotEmpty(t, tokenResponse.Token)
+
+ anonymousToken = fmt.Sprintf("Bearer %s", tokenResponse.Token)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
+ addTokenAuthHeader(req, anonymousToken)
+ resp = MakeRequest(t, req, http.StatusOK)
+ })
+
+ t.Run("User", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
+ resp := MakeRequest(t, req, http.StatusUnauthorized)
+
+ assert.ElementsMatch(t, authenticate, resp.Header().Values("WWW-Authenticate"))
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%sv2/token", setting.AppURL))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ tokenResponse := &TokenResponse{}
+ DecodeJSON(t, resp, &tokenResponse)
+
+ assert.NotEmpty(t, tokenResponse.Token)
+
+ userToken = fmt.Sprintf("Bearer %s", tokenResponse.Token)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
+ addTokenAuthHeader(req, userToken)
+ resp = MakeRequest(t, req, http.StatusOK)
+ })
+ })
+
+ t.Run("DetermineSupport", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%sv2", setting.AppURL))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+ assert.Equal(t, "registry/2.0", resp.Header().Get("Docker-Distribution-Api-Version"))
+ })
+
+ for _, image := range images {
+ t.Run(fmt.Sprintf("[Image:%s]", image), func(t *testing.T) {
+ url := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, image)
+
+ t.Run("UploadBlob/Monolithic", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url))
+ addTokenAuthHeader(req, anonymousToken)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, unknownDigest), bytes.NewReader(blobContent))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, blobDigest), bytes.NewReader(blobContent))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location"))
+ assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest"))
+
+ pv, err := packages_model.GetInternalVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, container_model.UploadVersion)
+ assert.NoError(t, err)
+
+ pfs, err := packages_model.GetFilesByVersionID(db.DefaultContext, pv.ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+
+ pb, err := packages_model.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.EqualValues(t, len(blobContent), pb.Size)
+ })
+
+ t.Run("UploadBlob/Chunked", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads", url))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusAccepted)
+
+ uuid := resp.Header().Get("Docker-Upload-Uuid")
+ assert.NotEmpty(t, uuid)
+
+ pbu, err := packages_model.GetBlobUploadByID(db.DefaultContext, uuid)
+ assert.NoError(t, err)
+ assert.EqualValues(t, 0, pbu.BytesReceived)
+
+ uploadURL := resp.Header().Get("Location")
+ assert.NotEmpty(t, uploadURL)
+
+ req = NewRequestWithBody(t, "PATCH", setting.AppURL+uploadURL[1:]+"000", bytes.NewReader(blobContent))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequestWithBody(t, "PATCH", setting.AppURL+uploadURL[1:], bytes.NewReader(blobContent))
+ addTokenAuthHeader(req, userToken)
+
+ req.Header.Set("Content-Range", "1-10")
+ MakeRequest(t, req, http.StatusRequestedRangeNotSatisfiable)
+
+ contentRange := fmt.Sprintf("0-%d", len(blobContent)-1)
+ req.Header.Set("Content-Range", contentRange)
+ resp = MakeRequest(t, req, http.StatusAccepted)
+
+ assert.Equal(t, uuid, resp.Header().Get("Docker-Upload-Uuid"))
+ assert.Equal(t, contentRange, resp.Header().Get("Range"))
+
+ pbu, err = packages_model.GetBlobUploadByID(db.DefaultContext, uuid)
+ assert.NoError(t, err)
+ assert.EqualValues(t, len(blobContent), pbu.BytesReceived)
+
+ uploadURL = resp.Header().Get("Location")
+
+ req = NewRequest(t, "PUT", fmt.Sprintf("%s?digest=%s", setting.AppURL+uploadURL[1:], blobDigest))
+ addTokenAuthHeader(req, userToken)
+ resp = MakeRequest(t, req, http.StatusCreated)
+
+ assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location"))
+ assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest"))
+ })
+
+ for _, tag := range tags {
+ t.Run(fmt.Sprintf("[Tag:%s]", tag), func(t *testing.T) {
+ t.Run("UploadManifest", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, configDigest), strings.NewReader(configContent))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusCreated)
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent))
+ addTokenAuthHeader(req, anonymousToken)
+ req.Header.Set("Content-Type", oci.MediaTypeDockerManifest)
+ MakeRequest(t, req, http.StatusUnauthorized)
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent))
+ addTokenAuthHeader(req, userToken)
+ req.Header.Set("Content-Type", oci.MediaTypeDockerManifest)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest"))
+
+ pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, tag)
+ assert.NoError(t, err)
+
+ pd, err := packages_model.GetPackageDescriptor(db.DefaultContext, pv)
+ assert.NoError(t, err)
+ assert.Nil(t, pd.SemVer)
+ assert.Equal(t, image, pd.Package.Name)
+ assert.Equal(t, tag, pd.Version.Version)
+ assert.True(t, has(pd.Properties, container_module.PropertyManifestTagged))
+
+ assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
+ metadata := pd.Metadata.(*container_module.Metadata)
+ assert.Equal(t, container_module.TypeOCI, metadata.Type)
+ assert.Len(t, metadata.ImageLayers, 2)
+ assert.Empty(t, metadata.MultiArch)
+
+ assert.Len(t, pd.Files, 3)
+ for _, pfd := range pd.Files {
+ switch pfd.File.Name {
+ case container_model.ManifestFilename:
+ assert.True(t, pfd.File.IsLead)
+ assert.Equal(t, oci.MediaTypeDockerManifest, pfd.Properties.GetByName(container_module.PropertyMediaType))
+ assert.Equal(t, manifestDigest, pfd.Properties.GetByName(container_module.PropertyDigest))
+ case strings.Replace(configDigest, ":", "_", 1):
+ assert.False(t, pfd.File.IsLead)
+ assert.Equal(t, "application/vnd.docker.container.image.v1+json", pfd.Properties.GetByName(container_module.PropertyMediaType))
+ assert.Equal(t, configDigest, pfd.Properties.GetByName(container_module.PropertyDigest))
+ case strings.Replace(blobDigest, ":", "_", 1):
+ assert.False(t, pfd.File.IsLead)
+ assert.Equal(t, "application/vnd.docker.image.rootfs.diff.tar.gzip", pfd.Properties.GetByName(container_module.PropertyMediaType))
+ assert.Equal(t, blobDigest, pfd.Properties.GetByName(container_module.PropertyDigest))
+ default:
+ assert.Fail(t, "unknown file: %s", pfd.File.Name)
+ }
+ }
+
+ // Overwrite existing tag
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, tag), strings.NewReader(manifestContent))
+ addTokenAuthHeader(req, userToken)
+ req.Header.Set("Content-Type", oci.MediaTypeDockerManifest)
+ MakeRequest(t, req, http.StatusCreated)
+ })
+
+ t.Run("HeadManifest", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/unknown-tag", url))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/%s", url, tag))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, fmt.Sprintf("%d", len(manifestContent)), resp.Header().Get("Content-Length"))
+ assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest"))
+ })
+
+ t.Run("GetManifest", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/manifests/unknown-tag", url))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, fmt.Sprintf("%d", len(manifestContent)), resp.Header().Get("Content-Length"))
+ assert.Equal(t, oci.MediaTypeDockerManifest, resp.Header().Get("Content-Type"))
+ assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest"))
+ assert.Equal(t, manifestContent, resp.Body.String())
+ })
+ })
+ }
+
+ t.Run("UploadUntaggedManifest", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, untaggedManifestDigest), strings.NewReader(untaggedManifestContent))
+ addTokenAuthHeader(req, userToken)
+ req.Header.Set("Content-Type", oci.MediaTypeImageManifest)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ assert.Equal(t, untaggedManifestDigest, resp.Header().Get("Docker-Content-Digest"))
+
+ req = NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/%s", url, untaggedManifestDigest))
+ addTokenAuthHeader(req, userToken)
+ resp = MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, fmt.Sprintf("%d", len(untaggedManifestContent)), resp.Header().Get("Content-Length"))
+ assert.Equal(t, untaggedManifestDigest, resp.Header().Get("Docker-Content-Digest"))
+
+ pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, untaggedManifestDigest)
+ assert.NoError(t, err)
+
+ pd, err := packages_model.GetPackageDescriptor(db.DefaultContext, pv)
+ assert.NoError(t, err)
+ assert.Nil(t, pd.SemVer)
+ assert.Equal(t, image, pd.Package.Name)
+ assert.Equal(t, untaggedManifestDigest, pd.Version.Version)
+ assert.False(t, has(pd.Properties, container_module.PropertyManifestTagged))
+
+ assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
+
+ assert.Len(t, pd.Files, 3)
+ for _, pfd := range pd.Files {
+ if pfd.File.Name == container_model.ManifestFilename {
+ assert.True(t, pfd.File.IsLead)
+ assert.Equal(t, oci.MediaTypeImageManifest, pfd.Properties.GetByName(container_module.PropertyMediaType))
+ assert.Equal(t, untaggedManifestDigest, pfd.Properties.GetByName(container_module.PropertyDigest))
+ }
+ }
+ })
+
+ t.Run("UploadIndexManifest", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/%s", url, multiTag), strings.NewReader(indexManifestContent))
+ addTokenAuthHeader(req, userToken)
+ req.Header.Set("Content-Type", oci.MediaTypeImageIndex)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ assert.Equal(t, indexManifestDigest, resp.Header().Get("Docker-Content-Digest"))
+
+ pv, err := packages_model.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages_model.TypeContainer, image, multiTag)
+ assert.NoError(t, err)
+
+ pd, err := packages_model.GetPackageDescriptor(db.DefaultContext, pv)
+ assert.NoError(t, err)
+ assert.Nil(t, pd.SemVer)
+ assert.Equal(t, image, pd.Package.Name)
+ assert.Equal(t, multiTag, pd.Version.Version)
+ assert.True(t, has(pd.Properties, container_module.PropertyManifestTagged))
+
+ getAllByName := func(l packages_model.PackagePropertyList, name string) []string {
+ values := make([]string, 0, len(l))
+ for _, pp := range l {
+ if pp.Name == name {
+ values = append(values, pp.Value)
+ }
+ }
+ return values
+ }
+ assert.ElementsMatch(t, []string{manifestDigest, untaggedManifestDigest}, getAllByName(pd.Properties, container_module.PropertyManifestReference))
+
+ assert.IsType(t, &container_module.Metadata{}, pd.Metadata)
+ metadata := pd.Metadata.(*container_module.Metadata)
+ assert.Equal(t, container_module.TypeOCI, metadata.Type)
+ assert.Contains(t, metadata.MultiArch, "linux/arm/v7")
+ assert.Equal(t, manifestDigest, metadata.MultiArch["linux/arm/v7"])
+ assert.Contains(t, metadata.MultiArch, "linux/arm64/v8")
+ assert.Equal(t, untaggedManifestDigest, metadata.MultiArch["linux/arm64/v8"])
+
+ assert.Len(t, pd.Files, 1)
+ assert.True(t, pd.Files[0].File.IsLead)
+ assert.Equal(t, oci.MediaTypeImageIndex, pd.Files[0].Properties.GetByName(container_module.PropertyMediaType))
+ assert.Equal(t, indexManifestDigest, pd.Files[0].Properties.GetByName(container_module.PropertyDigest))
+ })
+
+ t.Run("UploadBlob/Mount", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads?mount=%s", url, unknownDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusAccepted)
+
+ req = NewRequest(t, "POST", fmt.Sprintf("%s/blobs/uploads?mount=%s", url, blobDigest))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusCreated)
+
+ assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, blobDigest), resp.Header().Get("Location"))
+ assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest"))
+ })
+
+ t.Run("HeadBlob", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, unknownDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, fmt.Sprintf("%d", len(blobContent)), resp.Header().Get("Content-Length"))
+ assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest"))
+ })
+
+ t.Run("GetBlob", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, unknownDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/blobs/%s", url, blobDigest))
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, fmt.Sprintf("%d", len(blobContent)), resp.Header().Get("Content-Length"))
+ assert.Equal(t, blobDigest, resp.Header().Get("Docker-Content-Digest"))
+ assert.Equal(t, blobContent, resp.Body.Bytes())
+ })
+
+ t.Run("GetTagList", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ cases := []struct {
+ URL string
+ ExpectedTags []string
+ ExpectedLink string
+ }{
+ {
+ URL: fmt.Sprintf("%s/tags/list", url),
+ ExpectedTags: []string{"latest", "main", "multi"},
+ ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
+ },
+ {
+ URL: fmt.Sprintf("%s/tags/list?n=0", url),
+ ExpectedTags: []string{},
+ ExpectedLink: "",
+ },
+ {
+ URL: fmt.Sprintf("%s/tags/list?n=2", url),
+ ExpectedTags: []string{"latest", "main"},
+ ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=2>; rel="next"`, user.Name, image),
+ },
+ {
+ URL: fmt.Sprintf("%s/tags/list?last=main", url),
+ ExpectedTags: []string{"multi"},
+ ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
+ },
+ {
+ URL: fmt.Sprintf("%s/tags/list?n=1&last=latest", url),
+ ExpectedTags: []string{"main"},
+ ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=1>; rel="next"`, user.Name, image),
+ },
+ }
+
+ for _, c := range cases {
+ req := NewRequest(t, "GET", c.URL)
+ addTokenAuthHeader(req, userToken)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ type TagList struct {
+ Name string `json:"name"`
+ Tags []string `json:"tags"`
+ }
+
+ tagList := &TagList{}
+ DecodeJSON(t, resp, &tagList)
+
+ assert.Equal(t, user.Name+"/"+image, tagList.Name)
+ assert.Equal(t, c.ExpectedTags, tagList.Tags)
+ assert.Equal(t, c.ExpectedLink, resp.Header().Get("Link"))
+ }
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ t.Run("Blob", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/blobs/%s", url, blobDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusAccepted)
+
+ req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, blobDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+
+ t.Run("ManifestByDigest", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/manifests/%s", url, untaggedManifestDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusAccepted)
+
+ req = NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/%s", url, untaggedManifestDigest))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+
+ t.Run("ManifestByTag", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/manifests/%s", url, multiTag))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusAccepted)
+
+ req = NewRequest(t, "HEAD", fmt.Sprintf("%s/manifests/%s", url, multiTag))
+ addTokenAuthHeader(req, userToken)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+ })
+ })
+ }
+}
diff --git a/integrations/api_packages_generic_test.go b/integrations/api_packages_generic_test.go
new file mode 100644
index 0000000000..c507702eaa
--- /dev/null
+++ b/integrations/api_packages_generic_test.go
@@ -0,0 +1,109 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageGeneric(t *testing.T) {
+ defer prepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+
+ packageName := "te-st_pac.kage"
+ packageVersion := "1.0.3"
+ filename := "fi-le_na.me"
+ content := []byte{1, 2, 3}
+
+ url := fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, packageVersion, filename)
+
+ t.Run("Upload", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.Nil(t, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, filename, pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), pb.Size)
+ })
+
+ t.Run("UploadExists", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusBadRequest)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", url)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, content, resp.Body.Bytes())
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(1), pvs[0].DownloadCount)
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", url)
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeGeneric)
+ assert.NoError(t, err)
+ assert.Empty(t, pvs)
+ })
+
+ t.Run("DownloadNotExists", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", url)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+
+ t.Run("DeleteNotExists", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", url)
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+}
diff --git a/integrations/api_packages_maven_test.go b/integrations/api_packages_maven_test.go
new file mode 100644
index 0000000000..c7c4542685
--- /dev/null
+++ b/integrations/api_packages_maven_test.go
@@ -0,0 +1,205 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/packages/maven"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageMaven(t *testing.T) {
+ defer prepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+
+ groupID := "com.gitea"
+ artifactID := "test-project"
+ packageName := groupID + "-" + artifactID
+ packageVersion := "1.0.1"
+ packageDescription := "Test Description"
+
+ root := fmt.Sprintf("/api/packages/%s/maven/%s/%s", user.Name, strings.ReplaceAll(groupID, ".", "/"), artifactID)
+ filename := fmt.Sprintf("%s-%s.jar", packageName, packageVersion)
+
+ putFile := func(t *testing.T, path, content string, expectedStatus int) {
+ req := NewRequestWithBody(t, "PUT", root+path, strings.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, expectedStatus)
+ }
+
+ t.Run("Upload", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ putFile(t, fmt.Sprintf("/%s/%s", packageVersion, filename), "test", http.StatusCreated)
+ putFile(t, "/maven-metadata.xml", "test", http.StatusOK)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.Nil(t, pd.SemVer)
+ assert.Nil(t, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, filename, pfs[0].Name)
+ assert.False(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(4), pb.Size)
+ })
+
+ t.Run("UploadExists", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ putFile(t, fmt.Sprintf("/%s/%s", packageVersion, filename), "test", http.StatusBadRequest)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, []byte("test"), resp.Body.Bytes())
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(0), pvs[0].DownloadCount)
+ })
+
+ t.Run("UploadVerifySHA1", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ t.Run("Missmatch", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ putFile(t, fmt.Sprintf("/%s/%s.sha1", packageVersion, filename), "test", http.StatusBadRequest)
+ })
+ t.Run("Valid", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ putFile(t, fmt.Sprintf("/%s/%s.sha1", packageVersion, filename), "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3", http.StatusOK)
+ })
+ })
+
+ pomContent := `<?xml version="1.0"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <groupId>` + groupID + `</groupId>
+ <artifactId>` + artifactID + `</artifactId>
+ <version>` + packageVersion + `</version>
+ <description>` + packageDescription + `</description>
+</project>`
+
+ t.Run("UploadPOM", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.Nil(t, pd.Metadata)
+
+ putFile(t, fmt.Sprintf("/%s/%s.pom", packageVersion, filename), pomContent, http.StatusCreated)
+
+ pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err = packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.IsType(t, &maven.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageDescription, pd.Metadata.(*maven.Metadata).Description)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 2)
+ i := 0
+ if strings.HasSuffix(pfs[1].Name, ".pom") {
+ i = 1
+ }
+ assert.Equal(t, filename+".pom", pfs[i].Name)
+ assert.True(t, pfs[i].IsLead)
+ })
+
+ t.Run("DownloadPOM", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, []byte(pomContent), resp.Body.Bytes())
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(1), pvs[0].DownloadCount)
+ })
+
+ t.Run("DownloadChecksums", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/1.2.3/%s", root, filename))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ for key, checksum := range map[string]string{
+ "md5": "098f6bcd4621d373cade4e832627b4f6",
+ "sha1": "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
+ "sha256": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
+ "sha512": "ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff",
+ } {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.%s", root, packageVersion, filename, key))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, checksum, resp.Body.String())
+ }
+ })
+
+ t.Run("DownloadMetadata", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", root+"/maven-metadata.xml")
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ expectedMetadata := `<?xml version="1.0" encoding="UTF-8"?>` + "\n<metadata><groupId>com.gitea</groupId><artifactId>test-project</artifactId><versioning><release>1.0.1</release><latest>1.0.1</latest><versions><version>1.0.1</version></versions></versioning></metadata>"
+ assert.Equal(t, expectedMetadata, resp.Body.String())
+
+ for key, checksum := range map[string]string{
+ "md5": "6bee0cebaaa686d658adf3e7e16371a0",
+ "sha1": "8696abce499fe84d9ea93e5492abe7147e195b6c",
+ "sha256": "3f48322f81c4b2c3bb8649ae1e5c9801476162b520e1c2734ac06b2c06143208",
+ "sha512": "cb075aa2e2ef1a83cdc14dd1e08c505b72d633399b39e73a21f00f0deecb39a3e2c79f157c1163f8a3854828750706e0dec3a0f5e4778e91f8ec2cf351a855f2",
+ } {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/maven-metadata.xml.%s", root, key))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, checksum, resp.Body.String())
+ }
+ })
+}
diff --git a/integrations/api_packages_npm_test.go b/integrations/api_packages_npm_test.go
new file mode 100644
index 0000000000..28a3711939
--- /dev/null
+++ b/integrations/api_packages_npm_test.go
@@ -0,0 +1,222 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/packages/npm"
+ "code.gitea.io/gitea/modules/setting"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageNpm(t *testing.T) {
+ defer prepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+
+ token := fmt.Sprintf("Bearer %s", getTokenForLoggedInUser(t, loginUser(t, user.Name)))
+
+ packageName := "@scope/test-package"
+ packageVersion := "1.0.1-pre"
+ packageTag := "latest"
+ packageTag2 := "release"
+ packageAuthor := "KN4CK3R"
+ packageDescription := "Test Description"
+
+ data := "H4sIAAAAAAAA/ytITM5OTE/VL4DQelnF+XkMVAYGBgZmJiYK2MRBwNDcSIHB2NTMwNDQzMwAqA7IMDUxA9LUdgg2UFpcklgEdAql5kD8ogCnhwio5lJQUMpLzE1VslJQcihOzi9I1S9JLS7RhSYIJR2QgrLUouLM/DyQGkM9Az1D3YIiqExKanFyUWZBCVQ2BKhVwQVJDKwosbQkI78IJO/tZ+LsbRykxFXLNdA+HwWjYBSMgpENACgAbtAACAAA"
+ upload := `{
+ "_id": "` + packageName + `",
+ "name": "` + packageName + `",
+ "description": "` + packageDescription + `",
+ "dist-tags": {
+ "` + packageTag + `": "` + packageVersion + `"
+ },
+ "versions": {
+ "` + packageVersion + `": {
+ "name": "` + packageName + `",
+ "version": "` + packageVersion + `",
+ "description": "` + packageDescription + `",
+ "author": {
+ "name": "` + packageAuthor + `"
+ },
+ "dist": {
+ "integrity": "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==",
+ "shasum": "aaa7eaf852a948b0aa05afeda35b1badca155d90"
+ }
+ }
+ },
+ "_attachments": {
+ "` + packageName + `-` + packageVersion + `.tgz": {
+ "data": "` + data + `"
+ }
+ }
+ }`
+
+ root := fmt.Sprintf("/api/packages/%s/npm/%s", user.Name, url.QueryEscape(packageName))
+ tagsRoot := fmt.Sprintf("/api/packages/%s/npm/-/package/%s/dist-tags", user.Name, url.QueryEscape(packageName))
+ filename := fmt.Sprintf("%s-%s.tgz", strings.Split(packageName, "/")[1], packageVersion)
+
+ t.Run("Upload", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", root, strings.NewReader(upload))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &npm.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+ assert.Len(t, pd.Properties, 1)
+ assert.Equal(t, npm.TagProperty, pd.Properties[0].Name)
+ assert.Equal(t, packageTag, pd.Properties[0].Value)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, filename, pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(192), pb.Size)
+ })
+
+ t.Run("UploadExists", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", root, strings.NewReader(upload))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusBadRequest)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/-/%s/%s", root, packageVersion, filename))
+ req = addTokenAuthHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ b, _ := base64.StdEncoding.DecodeString(data)
+ assert.Equal(t, b, resp.Body.Bytes())
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNpm)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(1), pvs[0].DownloadCount)
+ })
+
+ t.Run("PackageMetadata", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/packages/%s/npm/%s", user.Name, "does-not-exist"))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", root)
+ req = addTokenAuthHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result npm.PackageMetadata
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, packageName, result.ID)
+ assert.Equal(t, packageName, result.Name)
+ assert.Equal(t, packageDescription, result.Description)
+ assert.Contains(t, result.DistTags, packageTag)
+ assert.Equal(t, packageVersion, result.DistTags[packageTag])
+ assert.Equal(t, packageAuthor, result.Author.Name)
+ assert.Contains(t, result.Versions, packageVersion)
+ pmv := result.Versions[packageVersion]
+ assert.Equal(t, fmt.Sprintf("%s@%s", packageName, packageVersion), pmv.ID)
+ assert.Equal(t, packageName, pmv.Name)
+ assert.Equal(t, packageDescription, pmv.Description)
+ assert.Equal(t, packageAuthor, pmv.Author.Name)
+ assert.Equal(t, "sha512-yA4FJsVhetynGfOC1jFf79BuS+jrHbm0fhh+aHzCQkOaOBXKf9oBnC4a6DnLLnEsHQDRLYd00cwj8sCXpC+wIg==", pmv.Dist.Integrity)
+ assert.Equal(t, "aaa7eaf852a948b0aa05afeda35b1badca155d90", pmv.Dist.Shasum)
+ assert.Equal(t, fmt.Sprintf("%s%s/-/%s/%s", setting.AppURL, root[1:], packageVersion, filename), pmv.Dist.Tarball)
+ })
+
+ t.Run("AddTag", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ test := func(t *testing.T, status int, tag, version string) {
+ req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/%s", tagsRoot, tag), strings.NewReader(`"`+version+`"`))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, status)
+ }
+
+ test(t, http.StatusBadRequest, "1.0", packageVersion)
+ test(t, http.StatusBadRequest, "v1.0", packageVersion)
+ test(t, http.StatusNotFound, packageTag2, "1.2")
+ test(t, http.StatusOK, packageTag, packageVersion)
+ test(t, http.StatusOK, packageTag2, packageVersion)
+ })
+
+ t.Run("ListTags", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", tagsRoot)
+ req = addTokenAuthHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result map[string]string
+ DecodeJSON(t, resp, &result)
+
+ assert.Len(t, result, 2)
+ assert.Contains(t, result, packageTag)
+ assert.Equal(t, packageVersion, result[packageTag])
+ assert.Contains(t, result, packageTag2)
+ assert.Equal(t, packageVersion, result[packageTag2])
+ })
+
+ t.Run("PackageMetadataDistTags", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", root)
+ req = addTokenAuthHeader(req, token)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result npm.PackageMetadata
+ DecodeJSON(t, resp, &result)
+
+ assert.Len(t, result.DistTags, 2)
+ assert.Contains(t, result.DistTags, packageTag)
+ assert.Equal(t, packageVersion, result.DistTags[packageTag])
+ assert.Contains(t, result.DistTags, packageTag2)
+ assert.Equal(t, packageVersion, result.DistTags[packageTag2])
+ })
+
+ t.Run("DeleteTag", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ test := func(t *testing.T, status int, tag string) {
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s", tagsRoot, tag))
+ req = addTokenAuthHeader(req, token)
+ MakeRequest(t, req, status)
+ }
+
+ test(t, http.StatusBadRequest, "v1.0")
+ test(t, http.StatusBadRequest, "1.0")
+ test(t, http.StatusOK, "dummy")
+ test(t, http.StatusOK, packageTag2)
+ })
+}
diff --git a/integrations/api_packages_nuget_test.go b/integrations/api_packages_nuget_test.go
new file mode 100644
index 0000000000..e69dd0ff9b
--- /dev/null
+++ b/integrations/api_packages_nuget_test.go
@@ -0,0 +1,381 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+ "archive/zip"
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ nuget_module "code.gitea.io/gitea/modules/packages/nuget"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/routers/api/packages/nuget"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageNuGet(t *testing.T) {
+ defer prepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+
+ packageName := "test.package"
+ packageVersion := "1.0.3"
+ packageAuthors := "KN4CK3R"
+ packageDescription := "Gitea Test Package"
+ symbolFilename := "test.pdb"
+ symbolID := "d910bb6948bd4c6cb40155bcf52c3c94"
+
+ var buf bytes.Buffer
+ archive := zip.NewWriter(&buf)
+ w, _ := archive.Create("package.nuspec")
+ w.Write([]byte(`<?xml version="1.0" encoding="utf-8"?>
+ <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
+ <metadata>
+ <id>` + packageName + `</id>
+ <version>` + packageVersion + `</version>
+ <authors>` + packageAuthors + `</authors>
+ <description>` + packageDescription + `</description>
+ <group targetFramework=".NETStandard2.0">
+ <dependency id="Microsoft.CSharp" version="4.5.0" />
+ </group>
+ </metadata>
+ </package>`))
+ archive.Close()
+ content := buf.Bytes()
+
+ url := fmt.Sprintf("/api/packages/%s/nuget", user.Name)
+
+ t.Run("ServiceIndex", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result nuget.ServiceIndexResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, "3.0.0", result.Version)
+ assert.NotEmpty(t, result.Resources)
+
+ root := setting.AppURL + url[1:]
+ for _, r := range result.Resources {
+ switch r.Type {
+ case "SearchQueryService":
+ fallthrough
+ case "SearchQueryService/3.0.0-beta":
+ fallthrough
+ case "SearchQueryService/3.0.0-rc":
+ assert.Equal(t, root+"/query", r.ID)
+ case "RegistrationsBaseUrl":
+ fallthrough
+ case "RegistrationsBaseUrl/3.0.0-beta":
+ fallthrough
+ case "RegistrationsBaseUrl/3.0.0-rc":
+ assert.Equal(t, root+"/registration", r.ID)
+ case "PackageBaseAddress/3.0.0":
+ assert.Equal(t, root+"/package", r.ID)
+ case "PackagePublish/2.0.0":
+ assert.Equal(t, root, r.ID)
+ }
+ }
+ })
+
+ t.Run("Upload", func(t *testing.T) {
+ t.Run("DependencyPackage", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &nuget_module.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion), pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(len(content)), pb.Size)
+
+ req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusBadRequest)
+ })
+
+ t.Run("SymbolPackage", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ createPackage := func(id, packageType string) io.Reader {
+ var buf bytes.Buffer
+ archive := zip.NewWriter(&buf)
+
+ w, _ := archive.Create("package.nuspec")
+ w.Write([]byte(`<?xml version="1.0" encoding="utf-8"?>
+ <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
+ <metadata>
+ <id>` + id + `</id>
+ <version>` + packageVersion + `</version>
+ <authors>` + packageAuthors + `</authors>
+ <description>` + packageDescription + `</description>
+ <packageTypes><packageType name="` + packageType + `" /></packageTypes>
+ </metadata>
+ </package>`))
+
+ w, _ = archive.Create(symbolFilename)
+ b, _ := base64.StdEncoding.DecodeString(`QlNKQgEAAQAAAAAADAAAAFBEQiB2MS4wAAAAAAAABgB8AAAAWAAAACNQZGIAAAAA1AAAAAgBAAAj
+fgAA3AEAAAQAAAAjU3RyaW5ncwAAAADgAQAABAAAACNVUwDkAQAAMAAAACNHVUlEAAAAFAIAACgB
+AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
+ w.Write(b)
+
+ archive.Close()
+ return &buf
+ }
+
+ req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage("unknown-package", "SymbolsPackage"))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage(packageName, "DummyPackage"))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage(packageName, "SymbolsPackage"))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &nuget_module.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 3)
+ for _, pf := range pfs {
+ switch pf.Name {
+ case fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion):
+ case fmt.Sprintf("%s.%s.snupkg", packageName, packageVersion):
+ assert.False(t, pf.IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(616), pb.Size)
+ case symbolFilename:
+ assert.False(t, pf.IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(160), pb.Size)
+
+ pps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID)
+ assert.NoError(t, err)
+ assert.Len(t, pps, 1)
+ assert.Equal(t, nuget_module.PropertySymbolID, pps[0].Name)
+ assert.Equal(t, symbolID, pps[0].Value)
+ default:
+ assert.Fail(t, "unexpected file: %v", pf.Name)
+ }
+ }
+
+ req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createPackage(packageName, "SymbolsPackage"))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusBadRequest)
+ })
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ checkDownloadCount := func(count int64) {
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, count, pvs[0].DownloadCount)
+ }
+
+ checkDownloadCount(0)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", url, packageName, packageVersion, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, content, resp.Body.Bytes())
+
+ checkDownloadCount(1)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.snupkg", url, packageName, packageVersion, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkDownloadCount(1)
+
+ t.Run("Symbol", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/symbols/%s/%sFFFFFFFF/gitea.pdb", url, symbolFilename, symbolID))
+ MakeRequest(t, req, http.StatusBadRequest)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/symbols/%s/%sFFFFFFFF/%s", url, symbolFilename, "00000000000000000000000000000000", symbolFilename))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/symbols/%s/%sFFFFFFFF/%s", url, symbolFilename, symbolID, symbolFilename))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ checkDownloadCount(1)
+ })
+ })
+
+ t.Run("SearchService", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ cases := []struct {
+ Query string
+ Skip int
+ Take int
+ ExpectedTotal int64
+ ExpectedResults int
+ }{
+ {"", 0, 0, 1, 1},
+ {"", 0, 10, 1, 1},
+ {"gitea", 0, 10, 0, 0},
+ {"test", 0, 10, 1, 1},
+ {"test", 1, 10, 1, 0},
+ }
+
+ for i, c := range cases {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s&skip=%d&take=%d", url, c.Query, c.Skip, c.Take))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result nuget.SearchResultResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, c.ExpectedTotal, result.TotalHits, "case %d: unexpected total hits", i)
+ assert.Len(t, result.Data, c.ExpectedResults, "case %d: unexpected result count", i)
+ }
+ })
+
+ t.Run("RegistrationService", func(t *testing.T) {
+ indexURL := fmt.Sprintf("%s%s/registration/%s/index.json", setting.AppURL, url[1:], packageName)
+ leafURL := fmt.Sprintf("%s%s/registration/%s/%s.json", setting.AppURL, url[1:], packageName, packageVersion)
+ contentURL := fmt.Sprintf("%s%s/package/%s/%s/%s.%s.nupkg", setting.AppURL, url[1:], packageName, packageVersion, packageName, packageVersion)
+
+ t.Run("RegistrationIndex", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/index.json", url, packageName))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result nuget.RegistrationIndexResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, indexURL, result.RegistrationIndexURL)
+ assert.Equal(t, 1, result.Count)
+ assert.Len(t, result.Pages, 1)
+ assert.Equal(t, indexURL, result.Pages[0].RegistrationPageURL)
+ assert.Equal(t, packageVersion, result.Pages[0].Lower)
+ assert.Equal(t, packageVersion, result.Pages[0].Upper)
+ assert.Equal(t, 1, result.Pages[0].Count)
+ assert.Len(t, result.Pages[0].Items, 1)
+ assert.Equal(t, packageName, result.Pages[0].Items[0].CatalogEntry.ID)
+ assert.Equal(t, packageVersion, result.Pages[0].Items[0].CatalogEntry.Version)
+ assert.Equal(t, packageAuthors, result.Pages[0].Items[0].CatalogEntry.Authors)
+ assert.Equal(t, packageDescription, result.Pages[0].Items[0].CatalogEntry.Description)
+ assert.Equal(t, leafURL, result.Pages[0].Items[0].CatalogEntry.CatalogLeafURL)
+ assert.Equal(t, contentURL, result.Pages[0].Items[0].CatalogEntry.PackageContentURL)
+ })
+
+ t.Run("RegistrationLeaf", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/%s.json", url, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result nuget.RegistrationLeafResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Equal(t, leafURL, result.RegistrationLeafURL)
+ assert.Equal(t, contentURL, result.PackageContentURL)
+ assert.Equal(t, indexURL, result.RegistrationIndexURL)
+ })
+ })
+
+ t.Run("PackageService", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/index.json", url, packageName))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result nuget.PackageVersionsResponse
+ DecodeJSON(t, resp, &result)
+
+ assert.Len(t, result.Versions, 1)
+ assert.Equal(t, packageVersion, result.Versions[0])
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
+ assert.NoError(t, err)
+ assert.Empty(t, pvs)
+ })
+
+ t.Run("DownloadNotExists", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", url, packageName, packageVersion, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.snupkg", url, packageName, packageVersion, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+
+ t.Run("DeleteNotExists", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s", url, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusNotFound)
+ })
+}
diff --git a/integrations/api_packages_pypi_test.go b/integrations/api_packages_pypi_test.go
new file mode 100644
index 0000000000..5d610df39d
--- /dev/null
+++ b/integrations/api_packages_pypi_test.go
@@ -0,0 +1,181 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "mime/multipart"
+ "net/http"
+ "regexp"
+ "strings"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/packages/pypi"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackagePyPI(t *testing.T) {
+ defer prepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+
+ packageName := "test-package"
+ packageVersion := "1.0.1"
+ packageAuthor := "KN4CK3R"
+ packageDescription := "Test Description"
+
+ content := "test"
+ hashSHA256 := "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
+
+ root := fmt.Sprintf("/api/packages/%s/pypi", user.Name)
+
+ uploadFile := func(t *testing.T, filename, content string, expectedStatus int) {
+ body := &bytes.Buffer{}
+ writer := multipart.NewWriter(body)
+ part, _ := writer.CreateFormFile("content", filename)
+ _, _ = io.Copy(part, strings.NewReader(content))
+
+ writer.WriteField("name", packageName)
+ writer.WriteField("version", packageVersion)
+ writer.WriteField("author", packageAuthor)
+ writer.WriteField("summary", packageDescription)
+ writer.WriteField("description", packageDescription)
+ writer.WriteField("sha256_digest", hashSHA256)
+ writer.WriteField("requires_python", "3.6")
+
+ _ = writer.Close()
+
+ req := NewRequestWithBody(t, "POST", root, body)
+ req.Header.Add("Content-Type", writer.FormDataContentType())
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, expectedStatus)
+ }
+
+ t.Run("Upload", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ filename := "test.whl"
+ uploadFile(t, filename, content, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePyPI)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, filename, pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(4), pb.Size)
+ })
+
+ t.Run("UploadAddFile", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ filename := "test.tar.gz"
+ uploadFile(t, filename, content, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePyPI)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &pypi.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 2)
+
+ pf, err := packages.GetFileForVersionByName(db.DefaultContext, pvs[0].ID, filename, packages.EmptyFileKey)
+ assert.NoError(t, err)
+ assert.Equal(t, filename, pf.Name)
+ assert.True(t, pf.IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(4), pb.Size)
+ })
+
+ t.Run("UploadHashMismatch", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ filename := "test2.whl"
+ uploadFile(t, filename, "dummy", http.StatusBadRequest)
+ })
+
+ t.Run("UploadExists", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ uploadFile(t, "test.whl", content, http.StatusBadRequest)
+ uploadFile(t, "test.tar.gz", content, http.StatusBadRequest)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ downloadFile := func(filename string) {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/files/%s/%s/%s", root, packageName, packageVersion, filename))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, []byte(content), resp.Body.Bytes())
+ }
+
+ downloadFile("test.whl")
+ downloadFile("test.tar.gz")
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypePyPI)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(2), pvs[0].DownloadCount)
+ })
+
+ t.Run("PackageMetadata", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/simple/%s", root, packageName))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ htmlDoc := NewHTMLParser(t, resp.Body)
+ nodes := htmlDoc.doc.Find("a").Nodes
+ assert.Len(t, nodes, 2)
+
+ hrefMatcher := regexp.MustCompile(fmt.Sprintf(`%s/files/%s/%s/test\..+#sha256-%s`, root, packageName, packageVersion, hashSHA256))
+
+ for _, a := range nodes {
+ for _, att := range a.Attr {
+ switch att.Key {
+ case "href":
+ assert.Regexp(t, hrefMatcher, att.Val)
+ case "data-requires-python":
+ assert.Equal(t, "3.6", att.Val)
+ default:
+ t.Fail()
+ }
+ }
+ }
+ })
+}
diff --git a/integrations/api_packages_rubygems_test.go b/integrations/api_packages_rubygems_test.go
new file mode 100644
index 0000000000..269bc953b4
--- /dev/null
+++ b/integrations/api_packages_rubygems_test.go
@@ -0,0 +1,226 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+ "bytes"
+ "encoding/base64"
+ "fmt"
+ "mime/multipart"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ "code.gitea.io/gitea/modules/packages/rubygems"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageRubyGems(t *testing.T) {
+ defer prepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+
+ packageName := "gitea"
+ packageVersion := "1.0.5"
+ packageFilename := "gitea-1.0.5.gem"
+
+ gemContent, _ := base64.StdEncoding.DecodeString(`bWV0YWRhdGEuZ3oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDA0NDQAMDAwMDAw
+MAAwMDAwMDAwADAwMDAwMDAxMDQxADE0MTEwNzcyMzY2ADAxMzQ0MQAgMAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMHdoZWVsAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAd2hlZWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAw
+MDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf
+iwgA9vQjYQID1VVNb9QwEL37V5he9pRsmlJAFlQckCoOXAriQIUix5nNmsYf2JOqKwS/nYmz2d3Q
+qqCCKpFdadfjmfdm5nmcLMv4k9DXm6Wrv4BCcQ5GiPcelF5pJVE7y6w0IHirESS7hhDJJu4I+jhu
+Mc53Tsd5kZ8y30lcuWAEH2KY7HHtQhQs4+cJkwwuwNdeB6JhtbaNDoLTL1MQsFJrqQnr8jNrJJJH
+WZTHWfEiK094UYj0zYvp4Z9YAx5sA1ZpSCS3M30zeWwo2bG60FvUBjIKJts2GwMW76r0Yr9NzjN3
+YhwsGX2Ozl4dpcWwvK9d43PQtDIv9igvHwSyIIwFmXHjqTqxLY8MPkCADmQk80p2EfZ6VbM6/ue6
+/1D0Bq7/qeA/zh6W82leHmhFWUHn/JbsEfT6q7QbiCpoj8l0QcEUFLmX6kq2wBEiMjBSd+Pwt7T5
+Ot0kuXYMbkD1KOuOBnWYb7hBsAP4bhlkFRqnqpWefMZ/pHCn6+WIFGq2dgY8EQq+RvRRLJcTyZJ1
+WhHqGPTu7QdmACXdJFLwb9+ZdxErbSPKrqsMxJhAWCJ1qaqRdtu6yktcT/STsamG0qp7rsa5EL/K
+MBua30uw4ynzExqYWRJDfx8/kQWN3PwsDh2jYLr1W+pZcAmCs9splvnz/Flesqhbq21bXcGG/OLh
++2fv/JTF3hgZyCW9OaZjxoZjdnBGfgKpxZyJ1QYAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGF0
+YS50YXIuZ3oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDA0NDQAMDAwMDAwMAAw
+MDAwMDAwADAwMDAwMDAwMjQyADE0MTEwNzcyMzY2ADAxMzM2MQAgMAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB1c3RhcgAwMHdoZWVsAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAd2hlZWwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwMDAwADAwMDAwMDAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfiwgA
+9vQjYQID7M/NCsMgDABgz32KrA/QxersK/Q17ExXIcyhlr7+HLv1sJ02KPhBCPk5JOyn881nsl2c
+xI+gRDRaC3zbZ8RBCamlxGHolTFlX11kLwDFH6wp21hO2RYi/rD3bb5/7iCubFOCMbBtABzNkIjn
+bvGlAnisOUE7EnOALUR2p7b06e6aV4iqqqrquJ4AAAD//wMA+sA/NQAIAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGNoZWNr
+c3Vtcy55YW1sLmd6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwMDAwNDQ0ADAwMDAwMDAAMDAw
+MDAwMAAwMDAwMDAwMDQ1MAAxNDExMDc3MjM2NgAwMTQ2MTIAIDAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAdXN0YXIAMDB3aGVlbAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAHdoZWVsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDAwMDAwMAAwMDAwMDAwAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH4sIAPb0
+I2ECA2WQOa4UQAxE8znFXGCQ21vbPyMj5wRuL0Qk6EecnmZCyKyy9FSvXq/X4/u3ryj68Xg+f/Zn
+VHzGlx+/P57qvU4XxWalBKftSXOgCjNYkdRycrC5Axem+W4HqS12PNEv7836jF9vnlHxwSyxKY+y
+go0cPblyHzkrZ4HF1GSVhe7mOOoasXNk2fnbUxb+19Pp9tobD/QlJKMX7y204PREh6nQ5hG9Alw6
+x4TnmtA+aekGfm6wAseog2LSgpR4Q7cYnAH3K4qAQa6A6JCC1gpuY7P+9YxE5SZ+j0eVGbaBTwBQ
+iIqRUyyzLCoFCBdYNWxniapTavD97blXTzFvgoVoAsKBAtlU48cdaOmeZDpwV01OtcGwjscfeUrY
+B9QBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`)
+
+ root := fmt.Sprintf("/api/packages/%s/rubygems", user.Name)
+
+ uploadFile := func(t *testing.T, expectedStatus int) {
+ req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/api/v1/gems", root), bytes.NewReader(gemContent))
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, expectedStatus)
+ }
+
+ t.Run("Upload", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ uploadFile(t, http.StatusCreated)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRubyGems)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+
+ pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
+ assert.NoError(t, err)
+ assert.NotNil(t, pd.SemVer)
+ assert.IsType(t, &rubygems.Metadata{}, pd.Metadata)
+ assert.Equal(t, packageName, pd.Package.Name)
+ assert.Equal(t, packageVersion, pd.Version.Version)
+
+ pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
+ assert.NoError(t, err)
+ assert.Len(t, pfs, 1)
+ assert.Equal(t, packageFilename, pfs[0].Name)
+ assert.True(t, pfs[0].IsLead)
+
+ pb, err := packages.GetBlobByID(db.DefaultContext, pfs[0].BlobID)
+ assert.NoError(t, err)
+ assert.Equal(t, int64(4608), pb.Size)
+ })
+
+ t.Run("UploadExists", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ uploadFile(t, http.StatusBadRequest)
+ })
+
+ t.Run("Download", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/gems/%s", root, packageFilename))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, gemContent, resp.Body.Bytes())
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRubyGems)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(1), pvs[0].DownloadCount)
+ })
+
+ t.Run("DownloadGemspec", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/quick/Marshal.4.8/%sspec.rz", root, packageFilename))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ b, _ := base64.StdEncoding.DecodeString(`eJxi4Si1EndPzbWyCi5ITc5My0xOLMnMz2M8zMIRLeGpxGWsZ6RnzGbF5hqSyempxJWeWZKayGbN
+EBJqJQjWFZZaVJyZnxfN5qnEZahnoGcKkjTwVBJyB6lUKEhMzk5MTwULGngqcRaVJlWCONEMBp5K
+DGAWSKc7zFhPJamg0qRK99TcYphehZLU4hKInFhGSUlBsZW+PtgZepn5+iDxECRzDUDGcfh6hoA4
+gAAAAP//MS06Gw==`)
+ assert.Equal(t, b, resp.Body.Bytes())
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRubyGems)
+ assert.NoError(t, err)
+ assert.Len(t, pvs, 1)
+ assert.Equal(t, int64(1), pvs[0].DownloadCount)
+ })
+
+ t.Run("EnumeratePackages", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ enumeratePackages := func(t *testing.T, endpoint string, expectedContent []byte) {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/%s", root, endpoint))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ assert.Equal(t, expectedContent, resp.Body.Bytes())
+ }
+
+ b, _ := base64.StdEncoding.DecodeString(`H4sICAAAAAAA/3NwZWNzLjQuOABi4Yhmi+bwVOJKzyxJTWSzYnMNCbUSdE/NtbIKSy0qzszPi2bzVOIy1DPQM2WzZgjxVOIsKk2qBDEBAQAA///xOEYKOwAAAA==`)
+ enumeratePackages(t, "specs.4.8.gz", b)
+ b, _ = base64.StdEncoding.DecodeString(`H4sICAAAAAAA/2xhdGVzdF9zcGVjcy40LjgAYuGIZovm8FTiSs8sSU1ks2JzDQm1EnRPzbWyCkstKs7Mz4tm81TiMtQz0DNls2YI8VTiLCpNqgQxAQEAAP//8ThGCjsAAAA=`)
+ enumeratePackages(t, "latest_specs.4.8.gz", b)
+ b, _ = base64.StdEncoding.DecodeString(`H4sICAAAAAAA/3ByZXJlbGVhc2Vfc3BlY3MuNC44AGLhiGYABAAA//9snXr5BAAAAA==`)
+ enumeratePackages(t, "prerelease_specs.4.8.gz", b)
+ })
+
+ t.Run("Delete", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ body := bytes.Buffer{}
+ writer := multipart.NewWriter(&body)
+ writer.WriteField("gem_name", packageName)
+ writer.WriteField("version", packageVersion)
+ writer.Close()
+
+ req := NewRequestWithBody(t, "DELETE", fmt.Sprintf("%s/api/v1/gems/yank", root), &body)
+ req.Header.Add("Content-Type", writer.FormDataContentType())
+ req = AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusOK)
+
+ pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRubyGems)
+ assert.NoError(t, err)
+ assert.Empty(t, pvs)
+ })
+}
diff --git a/integrations/api_packages_test.go b/integrations/api_packages_test.go
new file mode 100644
index 0000000000..263e7cea53
--- /dev/null
+++ b/integrations/api_packages_test.go
@@ -0,0 +1,102 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "code.gitea.io/gitea/models/packages"
+ "code.gitea.io/gitea/models/unittest"
+ user_model "code.gitea.io/gitea/models/user"
+ api "code.gitea.io/gitea/modules/structs"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPackageAPI(t *testing.T) {
+ defer prepareTestEnv(t)()
+ user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}).(*user_model.User)
+ session := loginUser(t, user.Name)
+ token := getTokenForLoggedInUser(t, session)
+
+ packageName := "test-package"
+ packageVersion := "1.0.3"
+ filename := "file.bin"
+
+ url := fmt.Sprintf("/api/packages/%s/generic/%s/%s/%s", user.Name, packageName, packageVersion, filename)
+ req := NewRequestWithBody(t, "PUT", url, bytes.NewReader([]byte{}))
+ AddBasicAuthHeader(req, user.Name)
+ MakeRequest(t, req, http.StatusCreated)
+
+ t.Run("ListPackages", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s?token=%s", user.Name, token))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var apiPackages []*api.Package
+ DecodeJSON(t, resp, &apiPackages)
+
+ assert.Len(t, apiPackages, 1)
+ assert.Equal(t, string(packages.TypeGeneric), apiPackages[0].Type)
+ assert.Equal(t, packageName, apiPackages[0].Name)
+ assert.Equal(t, packageVersion, apiPackages[0].Version)
+ assert.NotNil(t, apiPackages[0].Creator)
+ assert.Equal(t, user.Name, apiPackages[0].Creator.UserName)
+ })
+
+ t.Run("GetPackage", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/dummy/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var p *api.Package
+ DecodeJSON(t, resp, &p)
+
+ assert.Equal(t, string(packages.TypeGeneric), p.Type)
+ assert.Equal(t, packageName, p.Name)
+ assert.Equal(t, packageVersion, p.Version)
+ assert.NotNil(t, p.Creator)
+ assert.Equal(t, user.Name, p.Creator.UserName)
+ })
+
+ t.Run("ListPackageFiles", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/dummy/%s/%s/files?token=%s", user.Name, packageName, packageVersion, token))
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s/files?token=%s", user.Name, packageName, packageVersion, token))
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var files []*api.PackageFile
+ DecodeJSON(t, resp, &files)
+
+ assert.Len(t, files, 1)
+ assert.Equal(t, int64(0), files[0].Size)
+ assert.Equal(t, filename, files[0].Name)
+ assert.Equal(t, "d41d8cd98f00b204e9800998ecf8427e", files[0].HashMD5)
+ assert.Equal(t, "da39a3ee5e6b4b0d3255bfef95601890afd80709", files[0].HashSHA1)
+ assert.Equal(t, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", files[0].HashSHA256)
+ assert.Equal(t, "cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e", files[0].HashSHA512)
+ })
+
+ t.Run("DeletePackage", func(t *testing.T) {
+ defer PrintCurrentTest(t)()
+
+ req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/packages/%s/dummy/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+ MakeRequest(t, req, http.StatusNotFound)
+
+ req = NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/packages/%s/generic/%s/%s?token=%s", user.Name, packageName, packageVersion, token))
+ MakeRequest(t, req, http.StatusNoContent)
+ })
+}