From 6cb8c81de1c9dd8c83b62f2816e8af976914c154 Mon Sep 17 00:00:00 2001 From: "Haoyuan (Bill) Xing" Date: Sun, 19 May 2024 23:30:41 +0000 Subject: [PATCH] Add minimal implementation for RubyGems compact index API. (#3811) Current package registry for RubyGems does not work with Bundler, because it implements neither the [compact index](https://guides.rubygems.org/rubygems-org-compact-index-api/) or the [dependency API](https://guides.rubygems.org/rubygems-org-api/). As a result, bundler complains about finding non-existing dependencies when installing anything with dependency: `revealed dependencies not in the API or the lockfile`. This patch provides a minimal implementation for the compact index API to solve this issue. Specifically, we implemented a version that does not cache the results / do incremental updates; which is consistent with the current implementation. Testing: * Modified existing integration tests. * Manually Verified bundler is able to parse the served versions / info file. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/3811 Reviewed-by: Earl Warren Co-authored-by: Haoyuan (Bill) Xing Co-committed-by: Haoyuan (Bill) Xing --- release-notes/8.0.0/3811.md | 1 + routers/api/packages/api.go | 2 + routers/api/packages/rubygems/rubygems.go | 152 ++++++++++++++- .../integration/api_packages_rubygems_test.go | 178 +++++++++++++++++- 4 files changed, 323 insertions(+), 10 deletions(-) create mode 100644 release-notes/8.0.0/3811.md diff --git a/release-notes/8.0.0/3811.md b/release-notes/8.0.0/3811.md new file mode 100644 index 0000000000..e792ca4ec2 --- /dev/null +++ b/release-notes/8.0.0/3811.md @@ -0,0 +1 @@ +Implement a non-caching version of the [RubyGems compact API](https://guides.rubygems.org/rubygems-org-compact-index-api/) for bundler dependency resolution. diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 5e3cbac8f9..79285783b9 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -586,6 +586,8 @@ func CommonRoutes() *web.Route { r.Get("/specs.4.8.gz", rubygems.EnumeratePackages) r.Get("/latest_specs.4.8.gz", rubygems.EnumeratePackagesLatest) r.Get("/prerelease_specs.4.8.gz", rubygems.EnumeratePackagesPreRelease) + r.Get("/info/{package}", rubygems.ServePackageInfo) + r.Get("/versions", rubygems.ServeVersionsFile) r.Get("/quick/Marshal.4.8/{filename}", rubygems.ServePackageSpecification) r.Get("/gems/{filename}", rubygems.DownloadPackageFile) r.Group("/api/v1/gems", func() { diff --git a/routers/api/packages/rubygems/rubygems.go b/routers/api/packages/rubygems/rubygems.go index ba5f4de080..dfefe2c4fb 100644 --- a/routers/api/packages/rubygems/rubygems.go +++ b/routers/api/packages/rubygems/rubygems.go @@ -6,6 +6,7 @@ package rubygems import ( "compress/gzip" "compress/zlib" + "crypto/md5" "errors" "fmt" "io" @@ -22,6 +23,10 @@ import ( packages_service "code.gitea.io/gitea/services/packages" ) +const ( + Sep = "---\n" +) + func apiError(ctx *context.Context, status int, obj any) { helper.LogAndProcessError(ctx, status, obj, func(message string) { ctx.PlainText(status, message) @@ -92,6 +97,69 @@ func enumeratePackages(ctx *context.Context, filename string, pvs []*packages_mo } } +// Serves info file for rubygems.org compatible /info/{gem} file. +// See also https://guides.rubygems.org/rubygems-org-compact-index-api/. +func ServePackageInfo(ctx *context.Context) { + packageName := ctx.Params("package") + versions, err := packages_model.GetVersionsByPackageName( + ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems, packageName) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + } + if len(versions) == 0 { + apiError(ctx, http.StatusNotFound, fmt.Sprintf("Could not find package %s", packageName)) + } + + result, err := buildInfoFileForPackage(ctx, versions) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + + ctx.PlainText(http.StatusOK, *result) +} + +// ServeVersionsFile creates rubygems.org compatible /versions file. +// See also https://guides.rubygems.org/rubygems-org-compact-index-api/. +func ServeVersionsFile(ctx *context.Context) { + packages, err := packages_model.GetPackagesByType( + ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + return + } + result := new(strings.Builder) + result.WriteString(Sep) + for _, pack := range packages { + versions, err := packages_model.GetVersionsByPackageName( + ctx, ctx.Package.Owner.ID, packages_model.TypeRubyGems, pack.Name) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + } + if len(versions) == 0 { + // No versions left for this package, we should continue. + continue + } + + fmt.Fprintf(result, "%s ", pack.Name) + for i, v := range versions { + result.WriteString(v.Version) + if i != len(versions)-1 { + result.WriteString(",") + } + } + + info, err := buildInfoFileForPackage(ctx, versions) + if err != nil { + apiError(ctx, http.StatusInternalServerError, err) + } + + checksum := md5.Sum([]byte(*info)) + fmt.Fprintf(result, " %x\n", checksum) + } + ctx.PlainText(http.StatusOK, result.String()) +} + // ServePackageSpecification serves the compressed Gemspec file of a package func ServePackageSpecification(ctx *context.Context) { filename := ctx.Params("filename") @@ -227,12 +295,7 @@ func UploadPackageFile(ctx *context.Context) { return } - var filename string - if rp.Metadata.Platform == "" || rp.Metadata.Platform == "ruby" { - filename = strings.ToLower(fmt.Sprintf("%s-%s.gem", rp.Name, rp.Version)) - } else { - filename = strings.ToLower(fmt.Sprintf("%s-%s-%s.gem", rp.Name, rp.Version, rp.Metadata.Platform)) - } + filename := getFullFilename(rp.Name, rp.Version, rp.Metadata.Platform) _, _, err = packages_service.CreatePackageAndAddFile( ctx, @@ -300,6 +363,83 @@ func DeletePackage(ctx *context.Context) { } } +func writeRequirements(reqs []rubygems_module.VersionRequirement, result *strings.Builder) { + if len(reqs) == 0 { + reqs = []rubygems_module.VersionRequirement{{Restriction: ">=", Version: "0"}} + } + for i, req := range reqs { + if i != 0 { + result.WriteString("&") + } + result.WriteString(req.Restriction) + result.WriteString(" ") + result.WriteString(req.Version) + } +} + +func buildRequirementStringFromVersion(ctx *context.Context, version *packages_model.PackageVersion) (string, error) { + pd, err := packages_model.GetPackageDescriptor(ctx, version) + if err != nil { + return "", err + } + metadata := pd.Metadata.(*rubygems_module.Metadata) + dependencyRequirements := new(strings.Builder) + for i, dep := range metadata.RuntimeDependencies { + if i != 0 { + dependencyRequirements.WriteString(",") + } + + dependencyRequirements.WriteString(dep.Name) + dependencyRequirements.WriteString(":") + reqs := dep.Version + writeRequirements(reqs, dependencyRequirements) + } + fullname := getFullFilename(pd.Package.Name, version.Version, metadata.Platform) + file, err := packages_model.GetFileForVersionByName(ctx, version.ID, fullname, "") + if err != nil { + return "", err + } + blob, err := packages_model.GetBlobByID(ctx, file.BlobID) + if err != nil { + return "", err + } + additionalRequirements := new(strings.Builder) + fmt.Fprintf(additionalRequirements, "checksum:%s", blob.HashSHA256) + if len(metadata.RequiredRubyVersion) != 0 { + additionalRequirements.WriteString(",ruby:") + writeRequirements(metadata.RequiredRubyVersion, additionalRequirements) + } + if len(metadata.RequiredRubygemsVersion) != 0 { + additionalRequirements.WriteString(",rubygems:") + writeRequirements(metadata.RequiredRubygemsVersion, additionalRequirements) + } + return fmt.Sprintf("%s %s|%s", version.Version, dependencyRequirements, additionalRequirements), nil +} + +func buildInfoFileForPackage(ctx *context.Context, versions []*packages_model.PackageVersion) (*string, error) { + result := "---\n" + for _, v := range versions { + str, err := buildRequirementStringFromVersion(ctx, v) + if err != nil { + return nil, err + } + result += str + result += "\n" + } + return &result, nil +} + +func getFullFilename(gemName, version, platform string) string { + return strings.ToLower(getFullName(gemName, version, platform)) + ".gem" +} + +func getFullName(gemName, version, platform string) string { + if platform == "" || platform == "ruby" { + return fmt.Sprintf("%s-%s", gemName, version) + } + return fmt.Sprintf("%s-%s-%s", gemName, version, platform) +} + func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_model.PackageVersion, error) { pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ OwnerID: ctx.Package.Owner.ID, diff --git a/tests/integration/api_packages_rubygems_test.go b/tests/integration/api_packages_rubygems_test.go index 5670731c49..26f41d7061 100644 --- a/tests/integration/api_packages_rubygems_test.go +++ b/tests/integration/api_packages_rubygems_test.go @@ -5,10 +5,13 @@ package integration import ( "bytes" + "crypto/md5" + "crypto/sha256" "encoding/base64" "fmt" "mime/multipart" "net/http" + "strings" "testing" "code.gitea.io/gitea/models/db" @@ -29,6 +32,9 @@ func TestPackageRubyGems(t *testing.T) { packageName := "gitea" packageVersion := "1.0.5" packageFilename := "gitea-1.0.5.gem" + packageDependency := "runtime-dep:>= 1.2.0&< 2.0" + rubyRequirements := "ruby:>= 2.3.0" + sep := "---" gemContent, _ := base64.StdEncoding.DecodeString(`bWV0YWRhdGEuZ3oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDA0NDQAMDAwMDAw @@ -111,11 +117,93 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`) + checksum := fmt.Sprintf("%x", sha256.Sum256(gemContent)) + + holaPackageName := "hola" + holaPackageVersion := "0.0.1" + // holaPackageFilename := "hola-0.0.1.gem" + holaPackageDependency := "example:~> 1.1&>= 1.1.4,zero:>= 0" + holaRubyGemsRequirements := "rubygems:= 1.2.3" + // sep := "---" + + holaGemContent, _ := base64.StdEncoding.DecodeString(`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 +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABjaGVj +a3N1bXMueWFtbC5negAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMDAwMDQ0NAAwMDAwMDAwADAw +MDAwMDAAMDAwMDAwMDA0NTMAMTQ2MjIyNTUzNjQAMDE0NjE3ACAwAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHVzdGFyADAwd2hlZWwAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAB3aGVlbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAwMDAwMDAAMDAwMDAwMAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+LCAD0 +WklmAgNlkD2uFDAMBvs9xV5gke04/nkdHT0nsGObigZtxekJr4QijaWMZr7X6/X4/u0rbfl4PJ8/ ++x0V7/jy4/fH0xIRx4PkkBT7Qt8cG0DSNq2iBIkMwD0ETCCgQ6Xh5WZWeHmfrHf8+uSRBivatKy8 +n6UT249x1CbVnAEHsbSDNEJAfRDuOytsa27767mR/vPkPtXpakdXyRBdsXti1lkjiye7uCeyHath +7VzMRxAOxNo3L31AiJu7ryPe7BsZR91Tcp21chYaSyvDwZaQE+SxlOn09fqnM8nUVcUb5CSEbljA +LH4HrZhC5YJMNyRevKZtKiqTZroEkKvuoHmS0iYWQFXYnTqOz0Wpxqw6RqnHhf0uah45WkjqtfPx +B6h0MiLUAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==`) + holaChecksum := fmt.Sprintf("%x", sha256.Sum256(holaGemContent)) 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)). + uploadFile := func(t *testing.T, content []byte, expectedStatus int) { + req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/api/v1/gems", root), bytes.NewReader(content)). AddBasicAuth(user.Name) MakeRequest(t, req, expectedStatus) } @@ -123,7 +211,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`) t.Run("Upload", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - uploadFile(t, http.StatusCreated) + uploadFile(t, gemContent, http.StatusCreated) pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeRubyGems) assert.NoError(t, err) @@ -150,7 +238,7 @@ AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`) t.Run("UploadExists", func(t *testing.T) { defer tests.PrintCurrentTest(t)() - uploadFile(t, http.StatusConflict) + uploadFile(t, gemContent, http.StatusConflict) }) t.Run("Download", func(t *testing.T) { @@ -206,6 +294,72 @@ gAAAAP//MS06Gw==`) enumeratePackages(t, "prerelease_specs.4.8.gz", b) }) + t.Run("UploadHola", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + uploadFile(t, holaGemContent, http.StatusCreated) + }) + + t.Run("PackageInfo", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/info/%s", root, packageName)). + AddBasicAuth(user.Name) + resp := MakeRequest(t, req, http.StatusOK) + expected := fmt.Sprintf("%s\n%s %s|checksum:%s,%s\n", + sep, packageVersion, packageDependency, checksum, rubyRequirements) + assert.Equal(t, expected, resp.Body.String()) + }) + + t.Run("HolaPackageInfo", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/info/%s", root, holaPackageName)). + AddBasicAuth(user.Name) + resp := MakeRequest(t, req, http.StatusOK) + expected := fmt.Sprintf("%s\n%s %s|checksum:%s,%s\n", + sep, holaPackageVersion, holaPackageDependency, holaChecksum, holaRubyGemsRequirements) + assert.Equal(t, expected, resp.Body.String()) + }) + t.Run("Versions", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + versionsReq := NewRequest(t, "GET", fmt.Sprintf("%s/versions", root)). + AddBasicAuth(user.Name) + versionsResp := MakeRequest(t, versionsReq, http.StatusOK) + infoReq := NewRequest(t, "GET", fmt.Sprintf("%s/info/%s", root, packageName)). + AddBasicAuth(user.Name) + infoResp := MakeRequest(t, infoReq, http.StatusOK) + holaInfoReq := NewRequest(t, "GET", fmt.Sprintf("%s/info/%s", root, holaPackageName)). + AddBasicAuth(user.Name) + holaInfoResp := MakeRequest(t, holaInfoReq, http.StatusOK) + + // expected := fmt.Sprintf("%s\n%s %s %x\n", + // sep, packageName, packageVersion, md5.Sum(infoResp.Body.Bytes())) + lines := versionsResp.Body.String() + assert.ElementsMatch(t, strings.Split(lines, "\n"), []string{ + sep, + fmt.Sprintf("%s %s %x", packageName, packageVersion, md5.Sum(infoResp.Body.Bytes())), + fmt.Sprintf("%s %s %x", holaPackageName, holaPackageVersion, md5.Sum(holaInfoResp.Body.Bytes())), + "", + }) + }) + + t.Run("DeleteHola", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + body := bytes.Buffer{} + writer := multipart.NewWriter(&body) + writer.WriteField("gem_name", holaPackageName) + writer.WriteField("version", holaPackageVersion) + writer.Close() + + req := NewRequestWithBody(t, "DELETE", fmt.Sprintf("%s/api/v1/gems/yank", root), &body). + SetHeader("Content-Type", writer.FormDataContentType()). + AddBasicAuth(user.Name) + MakeRequest(t, req, http.StatusOK) + }) + t.Run("Delete", func(t *testing.T) { defer tests.PrintCurrentTest(t)() @@ -224,4 +378,20 @@ gAAAAP//MS06Gw==`) assert.NoError(t, err) assert.Empty(t, pvs) }) + + t.Run("NonExistingGem", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/info/%s", root, packageName)). + AddBasicAuth(user.Name) + _ = MakeRequest(t, req, http.StatusNotFound) + }) + t.Run("EmptyVersions", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + req := NewRequest(t, "GET", fmt.Sprintf("%s/versions", root)). + AddBasicAuth(user.Name) + resp := MakeRequest(t, req, http.StatusOK) + assert.Equal(t, sep+"\n", resp.Body.String()) + }) }