code.gitea.io/gitea@v1.22.3/tests/integration/api_packages_nuget_test.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package integration
     5  
     6  import (
     7  	"archive/zip"
     8  	"bytes"
     9  	"encoding/base64"
    10  	"encoding/xml"
    11  	"fmt"
    12  	"io"
    13  	"net/http"
    14  	"net/http/httptest"
    15  	neturl "net/url"
    16  	"strconv"
    17  	"testing"
    18  	"time"
    19  
    20  	auth_model "code.gitea.io/gitea/models/auth"
    21  	"code.gitea.io/gitea/models/db"
    22  	"code.gitea.io/gitea/models/packages"
    23  	"code.gitea.io/gitea/models/unittest"
    24  	user_model "code.gitea.io/gitea/models/user"
    25  	nuget_module "code.gitea.io/gitea/modules/packages/nuget"
    26  	"code.gitea.io/gitea/modules/setting"
    27  	"code.gitea.io/gitea/modules/structs"
    28  	"code.gitea.io/gitea/routers/api/packages/nuget"
    29  	packageService "code.gitea.io/gitea/services/packages"
    30  	"code.gitea.io/gitea/tests"
    31  
    32  	"github.com/stretchr/testify/assert"
    33  )
    34  
    35  func addNuGetAPIKeyHeader(req *RequestWrapper, token string) {
    36  	req.SetHeader("X-NuGet-ApiKey", token)
    37  }
    38  
    39  func decodeXML(t testing.TB, resp *httptest.ResponseRecorder, v any) {
    40  	t.Helper()
    41  
    42  	assert.NoError(t, xml.NewDecoder(resp.Body).Decode(v))
    43  }
    44  
    45  func TestPackageNuGet(t *testing.T) {
    46  	defer tests.PrepareTestEnv(t)()
    47  
    48  	type FeedEntryProperties struct {
    49  		Version                  string                      `xml:"Version"`
    50  		NormalizedVersion        string                      `xml:"NormalizedVersion"`
    51  		Authors                  string                      `xml:"Authors"`
    52  		Dependencies             string                      `xml:"Dependencies"`
    53  		Description              string                      `xml:"Description"`
    54  		VersionDownloadCount     nuget.TypedValue[int64]     `xml:"VersionDownloadCount"`
    55  		DownloadCount            nuget.TypedValue[int64]     `xml:"DownloadCount"`
    56  		PackageSize              nuget.TypedValue[int64]     `xml:"PackageSize"`
    57  		Created                  nuget.TypedValue[time.Time] `xml:"Created"`
    58  		LastUpdated              nuget.TypedValue[time.Time] `xml:"LastUpdated"`
    59  		Published                nuget.TypedValue[time.Time] `xml:"Published"`
    60  		ProjectURL               string                      `xml:"ProjectUrl,omitempty"`
    61  		ReleaseNotes             string                      `xml:"ReleaseNotes,omitempty"`
    62  		RequireLicenseAcceptance nuget.TypedValue[bool]      `xml:"RequireLicenseAcceptance"`
    63  		Title                    string                      `xml:"Title"`
    64  	}
    65  
    66  	type FeedEntry struct {
    67  		XMLName    xml.Name             `xml:"entry"`
    68  		Properties *FeedEntryProperties `xml:"properties"`
    69  		Content    string               `xml:",innerxml"`
    70  	}
    71  
    72  	type FeedEntryLink struct {
    73  		Rel  string `xml:"rel,attr"`
    74  		Href string `xml:"href,attr"`
    75  	}
    76  
    77  	type FeedResponse struct {
    78  		XMLName xml.Name        `xml:"feed"`
    79  		Links   []FeedEntryLink `xml:"link"`
    80  		Entries []*FeedEntry    `xml:"entry"`
    81  		Count   int64           `xml:"count"`
    82  	}
    83  
    84  	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
    85  	writeToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeWritePackage)
    86  	readToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadPackage)
    87  	badToken := getUserToken(t, user.Name, auth_model.AccessTokenScopeReadNotification)
    88  
    89  	packageName := "test.package"
    90  	packageVersion := "1.0.3"
    91  	packageAuthors := "KN4CK3R"
    92  	packageDescription := "Gitea Test Package"
    93  	symbolFilename := "test.pdb"
    94  	symbolID := "d910bb6948bd4c6cb40155bcf52c3c94"
    95  
    96  	createNuspec := func(id, version string) string {
    97  		return `<?xml version="1.0" encoding="utf-8"?>
    98  <package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
    99  	<metadata>
   100  		<id>` + id + `</id>
   101  		<version>` + version + `</version>
   102  		<authors>` + packageAuthors + `</authors>
   103  		<description>` + packageDescription + `</description>
   104  		<dependencies>
   105  			<group targetFramework=".NETStandard2.0">
   106  				<dependency id="Microsoft.CSharp" version="4.5.0" />
   107  			</group>
   108  		</dependencies>
   109  	</metadata>
   110  </package>`
   111  	}
   112  
   113  	createPackage := func(id, version string) *bytes.Buffer {
   114  		var buf bytes.Buffer
   115  		archive := zip.NewWriter(&buf)
   116  		w, _ := archive.Create("package.nuspec")
   117  		w.Write([]byte(createNuspec(id, version)))
   118  		archive.Close()
   119  		return &buf
   120  	}
   121  
   122  	content := createPackage(packageName, packageVersion).Bytes()
   123  
   124  	url := fmt.Sprintf("/api/packages/%s/nuget", user.Name)
   125  
   126  	t.Run("ServiceIndex", func(t *testing.T) {
   127  		t.Run("v2", func(t *testing.T) {
   128  			defer tests.PrintCurrentTest(t)()
   129  
   130  			privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate})
   131  
   132  			cases := []struct {
   133  				Owner          string
   134  				UseBasicAuth   bool
   135  				token          string
   136  				expectedStatus int
   137  			}{
   138  				{privateUser.Name, false, "", http.StatusOK},
   139  				{privateUser.Name, true, "", http.StatusOK},
   140  				{privateUser.Name, false, writeToken, http.StatusOK},
   141  				{privateUser.Name, false, readToken, http.StatusOK},
   142  				{privateUser.Name, false, badToken, http.StatusOK},
   143  				{user.Name, false, "", http.StatusOK},
   144  				{user.Name, true, "", http.StatusOK},
   145  				{user.Name, false, writeToken, http.StatusOK},
   146  				{user.Name, false, readToken, http.StatusOK},
   147  				{user.Name, false, badToken, http.StatusOK},
   148  			}
   149  
   150  			for _, c := range cases {
   151  				t.Run(c.Owner, func(t *testing.T) {
   152  					url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner)
   153  
   154  					req := NewRequest(t, "GET", url)
   155  					if c.UseBasicAuth {
   156  						req.AddBasicAuth(user.Name)
   157  					} else if c.token != "" {
   158  						addNuGetAPIKeyHeader(req, c.token)
   159  					}
   160  					resp := MakeRequest(t, req, c.expectedStatus)
   161  					if c.expectedStatus != http.StatusOK {
   162  						return
   163  					}
   164  
   165  					var result nuget.ServiceIndexResponseV2
   166  					decodeXML(t, resp, &result)
   167  
   168  					assert.Equal(t, setting.AppURL+url[1:], result.Base)
   169  					assert.Equal(t, "Packages", result.Workspace.Collection.Href)
   170  				})
   171  			}
   172  		})
   173  
   174  		t.Run("v3", func(t *testing.T) {
   175  			defer tests.PrintCurrentTest(t)()
   176  
   177  			privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate})
   178  
   179  			cases := []struct {
   180  				Owner          string
   181  				UseBasicAuth   bool
   182  				token          string
   183  				expectedStatus int
   184  			}{
   185  				{privateUser.Name, false, "", http.StatusOK},
   186  				{privateUser.Name, true, "", http.StatusOK},
   187  				{privateUser.Name, false, writeToken, http.StatusOK},
   188  				{privateUser.Name, false, readToken, http.StatusOK},
   189  				{privateUser.Name, false, badToken, http.StatusOK},
   190  				{user.Name, false, "", http.StatusOK},
   191  				{user.Name, true, "", http.StatusOK},
   192  				{user.Name, false, writeToken, http.StatusOK},
   193  				{user.Name, false, readToken, http.StatusOK},
   194  				{user.Name, false, badToken, http.StatusOK},
   195  			}
   196  
   197  			for _, c := range cases {
   198  				t.Run(c.Owner, func(t *testing.T) {
   199  					url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner)
   200  
   201  					req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
   202  					if c.UseBasicAuth {
   203  						req.AddBasicAuth(user.Name)
   204  					} else if c.token != "" {
   205  						addNuGetAPIKeyHeader(req, c.token)
   206  					}
   207  					resp := MakeRequest(t, req, c.expectedStatus)
   208  
   209  					if c.expectedStatus != http.StatusOK {
   210  						return
   211  					}
   212  
   213  					var result nuget.ServiceIndexResponseV3
   214  					DecodeJSON(t, resp, &result)
   215  
   216  					assert.Equal(t, "3.0.0", result.Version)
   217  					assert.NotEmpty(t, result.Resources)
   218  
   219  					root := setting.AppURL + url[1:]
   220  					for _, r := range result.Resources {
   221  						switch r.Type {
   222  						case "SearchQueryService":
   223  							fallthrough
   224  						case "SearchQueryService/3.0.0-beta":
   225  							fallthrough
   226  						case "SearchQueryService/3.0.0-rc":
   227  							assert.Equal(t, root+"/query", r.ID)
   228  						case "RegistrationsBaseUrl":
   229  							fallthrough
   230  						case "RegistrationsBaseUrl/3.0.0-beta":
   231  							fallthrough
   232  						case "RegistrationsBaseUrl/3.0.0-rc":
   233  							assert.Equal(t, root+"/registration", r.ID)
   234  						case "PackageBaseAddress/3.0.0":
   235  							assert.Equal(t, root+"/package", r.ID)
   236  						case "PackagePublish/2.0.0":
   237  							assert.Equal(t, root, r.ID)
   238  						}
   239  					}
   240  				})
   241  			}
   242  		})
   243  	})
   244  
   245  	t.Run("Upload", func(t *testing.T) {
   246  		t.Run("DependencyPackage", func(t *testing.T) {
   247  			defer tests.PrintCurrentTest(t)()
   248  
   249  			// create with username/password
   250  			req := NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
   251  				AddBasicAuth(user.Name)
   252  			MakeRequest(t, req, http.StatusCreated)
   253  
   254  			pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
   255  			assert.NoError(t, err)
   256  			assert.Len(t, pvs, 1, "Should have one version")
   257  
   258  			pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
   259  			assert.NoError(t, err)
   260  			assert.NotNil(t, pd.SemVer)
   261  			assert.IsType(t, &nuget_module.Metadata{}, pd.Metadata)
   262  			assert.Equal(t, packageName, pd.Package.Name)
   263  			assert.Equal(t, packageVersion, pd.Version.Version)
   264  
   265  			pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
   266  			assert.NoError(t, err)
   267  			assert.Len(t, pfs, 2, "Should have 2 files: nuget and nuspec")
   268  			for _, pf := range pfs {
   269  				switch pf.Name {
   270  				case fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion):
   271  					assert.True(t, pf.IsLead)
   272  
   273  					pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
   274  					assert.NoError(t, err)
   275  					assert.Equal(t, int64(len(content)), pb.Size)
   276  				case fmt.Sprintf("%s.nuspec", packageName):
   277  					assert.False(t, pf.IsLead)
   278  				default:
   279  					assert.Fail(t, "unexpected filename: %v", pf.Name)
   280  				}
   281  			}
   282  
   283  			req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
   284  				AddBasicAuth(user.Name)
   285  			MakeRequest(t, req, http.StatusConflict)
   286  
   287  			// delete the package
   288  			assert.NoError(t, packageService.DeletePackageVersionAndReferences(db.DefaultContext, pvs[0]))
   289  
   290  			// create failure with token without write access
   291  			req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
   292  				AddTokenAuth(readToken)
   293  			MakeRequest(t, req, http.StatusUnauthorized)
   294  
   295  			// create with token
   296  			req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
   297  				AddTokenAuth(writeToken)
   298  			MakeRequest(t, req, http.StatusCreated)
   299  
   300  			pvs, err = packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
   301  			assert.NoError(t, err)
   302  			assert.Len(t, pvs, 1, "Should have one version")
   303  
   304  			pd, err = packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
   305  			assert.NoError(t, err)
   306  			assert.NotNil(t, pd.SemVer)
   307  			assert.IsType(t, &nuget_module.Metadata{}, pd.Metadata)
   308  			assert.Equal(t, packageName, pd.Package.Name)
   309  			assert.Equal(t, packageVersion, pd.Version.Version)
   310  
   311  			pfs, err = packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
   312  			assert.NoError(t, err)
   313  			assert.Len(t, pfs, 2, "Should have 2 files: nuget and nuspec")
   314  			for _, pf := range pfs {
   315  				switch pf.Name {
   316  				case fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion):
   317  					assert.True(t, pf.IsLead)
   318  
   319  					pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
   320  					assert.NoError(t, err)
   321  					assert.Equal(t, int64(len(content)), pb.Size)
   322  				case fmt.Sprintf("%s.nuspec", packageName):
   323  					assert.False(t, pf.IsLead)
   324  				default:
   325  					assert.Fail(t, "unexpected filename: %v", pf.Name)
   326  				}
   327  			}
   328  
   329  			req = NewRequestWithBody(t, "PUT", url, bytes.NewReader(content)).
   330  				AddBasicAuth(user.Name)
   331  			MakeRequest(t, req, http.StatusConflict)
   332  		})
   333  
   334  		t.Run("SymbolPackage", func(t *testing.T) {
   335  			defer tests.PrintCurrentTest(t)()
   336  
   337  			createSymbolPackage := func(id, packageType string) io.Reader {
   338  				var buf bytes.Buffer
   339  				archive := zip.NewWriter(&buf)
   340  
   341  				w, _ := archive.Create("package.nuspec")
   342  				w.Write([]byte(`<?xml version="1.0" encoding="utf-8"?>
   343  				<package xmlns="http://schemas.microsoft.com/packaging/2013/05/nuspec.xsd">
   344  				<metadata>
   345  					<id>` + id + `</id>
   346  					<version>` + packageVersion + `</version>
   347  					<authors>` + packageAuthors + `</authors>
   348  					<description>` + packageDescription + `</description>
   349  					<packageTypes><packageType name="` + packageType + `" /></packageTypes>
   350  				</metadata>
   351  				</package>`))
   352  
   353  				w, _ = archive.Create(symbolFilename)
   354  				b, _ := base64.StdEncoding.DecodeString(`QlNKQgEAAQAAAAAADAAAAFBEQiB2MS4wAAAAAAAABgB8AAAAWAAAACNQZGIAAAAA1AAAAAgBAAAj
   355  fgAA3AEAAAQAAAAjU3RyaW5ncwAAAADgAQAABAAAACNVUwDkAQAAMAAAACNHVUlEAAAAFAIAACgB
   356  AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
   357  				w.Write(b)
   358  
   359  				archive.Close()
   360  				return &buf
   361  			}
   362  
   363  			req := NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createSymbolPackage("unknown-package", "SymbolsPackage")).
   364  				AddBasicAuth(user.Name)
   365  			MakeRequest(t, req, http.StatusNotFound)
   366  
   367  			req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createSymbolPackage(packageName, "DummyPackage")).
   368  				AddBasicAuth(user.Name)
   369  			MakeRequest(t, req, http.StatusBadRequest)
   370  
   371  			req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createSymbolPackage(packageName, "SymbolsPackage")).
   372  				AddBasicAuth(user.Name)
   373  			MakeRequest(t, req, http.StatusCreated)
   374  
   375  			pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
   376  			assert.NoError(t, err)
   377  			assert.Len(t, pvs, 1)
   378  
   379  			pd, err := packages.GetPackageDescriptor(db.DefaultContext, pvs[0])
   380  			assert.NoError(t, err)
   381  			assert.NotNil(t, pd.SemVer)
   382  			assert.IsType(t, &nuget_module.Metadata{}, pd.Metadata)
   383  			assert.Equal(t, packageName, pd.Package.Name)
   384  			assert.Equal(t, packageVersion, pd.Version.Version)
   385  
   386  			pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pvs[0].ID)
   387  			assert.NoError(t, err)
   388  			assert.Len(t, pfs, 4, "Should have 4 files: nupkg, snupkg, nuspec and pdb")
   389  			for _, pf := range pfs {
   390  				switch pf.Name {
   391  				case fmt.Sprintf("%s.%s.nupkg", packageName, packageVersion):
   392  					assert.True(t, pf.IsLead)
   393  
   394  					pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
   395  					assert.NoError(t, err)
   396  					assert.Equal(t, int64(412), pb.Size)
   397  				case fmt.Sprintf("%s.%s.snupkg", packageName, packageVersion):
   398  					assert.False(t, pf.IsLead)
   399  
   400  					pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
   401  					assert.NoError(t, err)
   402  					assert.Equal(t, int64(616), pb.Size)
   403  				case fmt.Sprintf("%s.nuspec", packageName):
   404  					assert.False(t, pf.IsLead)
   405  
   406  					pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
   407  					assert.NoError(t, err)
   408  					assert.Equal(t, int64(427), pb.Size)
   409  				case symbolFilename:
   410  					assert.False(t, pf.IsLead)
   411  
   412  					pb, err := packages.GetBlobByID(db.DefaultContext, pf.BlobID)
   413  					assert.NoError(t, err)
   414  					assert.Equal(t, int64(160), pb.Size)
   415  
   416  					pps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID)
   417  					assert.NoError(t, err)
   418  					assert.Len(t, pps, 1)
   419  					assert.Equal(t, nuget_module.PropertySymbolID, pps[0].Name)
   420  					assert.Equal(t, symbolID, pps[0].Value)
   421  				default:
   422  					assert.FailNow(t, "unexpected file: %v", pf.Name)
   423  				}
   424  			}
   425  
   426  			req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/symbolpackage", url), createSymbolPackage(packageName, "SymbolsPackage")).
   427  				AddBasicAuth(user.Name)
   428  			MakeRequest(t, req, http.StatusConflict)
   429  		})
   430  	})
   431  
   432  	t.Run("Download", func(t *testing.T) {
   433  		defer tests.PrintCurrentTest(t)()
   434  
   435  		checkDownloadCount := func(count int64) {
   436  			pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
   437  			assert.NoError(t, err)
   438  			assert.Len(t, pvs, 1)
   439  			assert.Equal(t, count, pvs[0].DownloadCount)
   440  		}
   441  
   442  		checkDownloadCount(0)
   443  
   444  		req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", url, packageName, packageVersion, packageName, packageVersion)).
   445  			AddBasicAuth(user.Name)
   446  		resp := MakeRequest(t, req, http.StatusOK)
   447  
   448  		assert.Equal(t, content, resp.Body.Bytes())
   449  
   450  		req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.nuspec", url, packageName, packageVersion, packageName)).
   451  			AddBasicAuth(user.Name)
   452  		resp = MakeRequest(t, req, http.StatusOK)
   453  
   454  		assert.Equal(t, createNuspec(packageName, packageVersion), resp.Body.String())
   455  
   456  		checkDownloadCount(1)
   457  
   458  		req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.snupkg", url, packageName, packageVersion, packageName, packageVersion)).
   459  			AddBasicAuth(user.Name)
   460  		MakeRequest(t, req, http.StatusOK)
   461  
   462  		checkDownloadCount(1)
   463  
   464  		t.Run("Symbol", func(t *testing.T) {
   465  			defer tests.PrintCurrentTest(t)()
   466  
   467  			req := NewRequest(t, "GET", fmt.Sprintf("%s/symbols/%s/%sFFFFFFFF/gitea.pdb", url, symbolFilename, symbolID))
   468  			MakeRequest(t, req, http.StatusBadRequest)
   469  
   470  			req = NewRequest(t, "GET", fmt.Sprintf("%s/symbols/%s/%sFFFFFFFF/%s", url, symbolFilename, "00000000000000000000000000000000", symbolFilename)).
   471  				AddBasicAuth(user.Name)
   472  			MakeRequest(t, req, http.StatusNotFound)
   473  
   474  			req = NewRequest(t, "GET", fmt.Sprintf("%s/symbols/%s/%sFFFFffff/%s", url, symbolFilename, symbolID, symbolFilename)).
   475  				AddBasicAuth(user.Name)
   476  			MakeRequest(t, req, http.StatusOK)
   477  
   478  			checkDownloadCount(1)
   479  		})
   480  	})
   481  
   482  	containsOneNextLink := func(t *testing.T, links []FeedEntryLink) func() bool {
   483  		return func() bool {
   484  			found := 0
   485  			for _, l := range links {
   486  				if l.Rel == "next" {
   487  					found++
   488  					u, err := neturl.Parse(l.Href)
   489  					assert.NoError(t, err)
   490  					q := u.Query()
   491  					assert.Contains(t, q, "$skip")
   492  					assert.Contains(t, q, "$top")
   493  					assert.Equal(t, "1", q.Get("$skip"))
   494  					assert.Equal(t, "1", q.Get("$top"))
   495  				}
   496  			}
   497  			return found == 1
   498  		}
   499  	}
   500  
   501  	t.Run("SearchService", func(t *testing.T) {
   502  		cases := []struct {
   503  			Query              string
   504  			Skip               int
   505  			Take               int
   506  			ExpectedTotal      int64
   507  			ExpectedResults    int
   508  			ExpectedExactMatch bool
   509  		}{
   510  			{"", 0, 0, 4, 4, false},
   511  			{"", 0, 10, 4, 4, false},
   512  			{"gitea", 0, 10, 0, 0, false},
   513  			{"test", 0, 10, 1, 1, false},
   514  			{"test", 1, 10, 1, 0, false},
   515  			{"almost.similar", 0, 0, 3, 3, true},
   516  		}
   517  
   518  		fakePackages := []string{
   519  			packageName,
   520  			"almost.similar.dependency",
   521  			"almost.similar",
   522  			"almost.similar.dependant",
   523  		}
   524  
   525  		for _, fakePackageName := range fakePackages {
   526  			req := NewRequestWithBody(t, "PUT", url, createPackage(fakePackageName, "1.0.99")).
   527  				AddBasicAuth(user.Name)
   528  			MakeRequest(t, req, http.StatusCreated)
   529  		}
   530  
   531  		t.Run("v2", func(t *testing.T) {
   532  			t.Run("Search()", func(t *testing.T) {
   533  				defer tests.PrintCurrentTest(t)()
   534  
   535  				for i, c := range cases {
   536  					req := NewRequest(t, "GET", fmt.Sprintf("%s/Search()?searchTerm='%s'&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take)).
   537  						AddBasicAuth(user.Name)
   538  					resp := MakeRequest(t, req, http.StatusOK)
   539  
   540  					var result FeedResponse
   541  					decodeXML(t, resp, &result)
   542  
   543  					assert.Equal(t, c.ExpectedTotal, result.Count, "case %d: unexpected total hits", i)
   544  					assert.Len(t, result.Entries, c.ExpectedResults, "case %d: unexpected result count", i)
   545  
   546  					req = NewRequest(t, "GET", fmt.Sprintf("%s/Search()/$count?searchTerm='%s'&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take)).
   547  						AddBasicAuth(user.Name)
   548  					resp = MakeRequest(t, req, http.StatusOK)
   549  
   550  					assert.Equal(t, strconv.FormatInt(c.ExpectedTotal, 10), resp.Body.String(), "case %d: unexpected total hits", i)
   551  				}
   552  			})
   553  
   554  			t.Run("Packages()", func(t *testing.T) {
   555  				defer tests.PrintCurrentTest(t)()
   556  
   557  				for i, c := range cases {
   558  					req := NewRequest(t, "GET", fmt.Sprintf("%s/Packages()?$filter=substringof('%s',tolower(Id))&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take)).
   559  						AddBasicAuth(user.Name)
   560  					resp := MakeRequest(t, req, http.StatusOK)
   561  
   562  					var result FeedResponse
   563  					decodeXML(t, resp, &result)
   564  
   565  					assert.Equal(t, c.ExpectedTotal, result.Count, "case %d: unexpected total hits", i)
   566  					assert.Len(t, result.Entries, c.ExpectedResults, "case %d: unexpected result count", i)
   567  
   568  					req = NewRequest(t, "GET", fmt.Sprintf("%s/Packages()/$count?$filter=substringof('%s',tolower(Id))&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take)).
   569  						AddBasicAuth(user.Name)
   570  					resp = MakeRequest(t, req, http.StatusOK)
   571  
   572  					assert.Equal(t, strconv.FormatInt(c.ExpectedTotal, 10), resp.Body.String(), "case %d: unexpected total hits", i)
   573  				}
   574  			})
   575  
   576  			t.Run("Packages()", func(t *testing.T) {
   577  				defer tests.PrintCurrentTest(t)()
   578  
   579  				t.Run("substringof", func(t *testing.T) {
   580  					defer tests.PrintCurrentTest(t)()
   581  
   582  					for i, c := range cases {
   583  						req := NewRequest(t, "GET", fmt.Sprintf("%s/Packages()?$filter=substringof('%s',tolower(Id))&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take)).
   584  							AddBasicAuth(user.Name)
   585  						resp := MakeRequest(t, req, http.StatusOK)
   586  
   587  						var result FeedResponse
   588  						decodeXML(t, resp, &result)
   589  
   590  						assert.Equal(t, c.ExpectedTotal, result.Count, "case %d: unexpected total hits", i)
   591  						assert.Len(t, result.Entries, c.ExpectedResults, "case %d: unexpected result count", i)
   592  
   593  						req = NewRequest(t, "GET", fmt.Sprintf("%s/Packages()/$count?$filter=substringof('%s',tolower(Id))&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take)).
   594  							AddBasicAuth(user.Name)
   595  						resp = MakeRequest(t, req, http.StatusOK)
   596  
   597  						assert.Equal(t, strconv.FormatInt(c.ExpectedTotal, 10), resp.Body.String(), "case %d: unexpected total hits", i)
   598  					}
   599  				})
   600  
   601  				t.Run("IdEq", func(t *testing.T) {
   602  					defer tests.PrintCurrentTest(t)()
   603  
   604  					for i, c := range cases {
   605  						if c.Query == "" {
   606  							// Ignore the `tolower(Id) eq ''` as it's unlikely to happen
   607  							continue
   608  						}
   609  						req := NewRequest(t, "GET", fmt.Sprintf("%s/Packages()?$filter=(tolower(Id) eq '%s')&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take)).
   610  							AddBasicAuth(user.Name)
   611  						resp := MakeRequest(t, req, http.StatusOK)
   612  
   613  						var result FeedResponse
   614  						decodeXML(t, resp, &result)
   615  
   616  						expectedCount := 0
   617  						if c.ExpectedExactMatch {
   618  							expectedCount = 1
   619  						}
   620  
   621  						assert.Equal(t, int64(expectedCount), result.Count, "case %d: unexpected total hits", i)
   622  						assert.Len(t, result.Entries, expectedCount, "case %d: unexpected result count", i)
   623  
   624  						req = NewRequest(t, "GET", fmt.Sprintf("%s/Packages()/$count?$filter=(tolower(Id) eq '%s')&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take)).
   625  							AddBasicAuth(user.Name)
   626  						resp = MakeRequest(t, req, http.StatusOK)
   627  
   628  						assert.Equal(t, strconv.FormatInt(int64(expectedCount), 10), resp.Body.String(), "case %d: unexpected total hits", i)
   629  					}
   630  				})
   631  			})
   632  
   633  			t.Run("Next", func(t *testing.T) {
   634  				req := NewRequest(t, "GET", fmt.Sprintf("%s/Search()?searchTerm='test'&$skip=0&$top=1", url)).
   635  					AddBasicAuth(user.Name)
   636  				resp := MakeRequest(t, req, http.StatusOK)
   637  
   638  				var result FeedResponse
   639  				decodeXML(t, resp, &result)
   640  
   641  				assert.Condition(t, containsOneNextLink(t, result.Links))
   642  			})
   643  		})
   644  
   645  		t.Run("v3", func(t *testing.T) {
   646  			defer tests.PrintCurrentTest(t)()
   647  
   648  			for i, c := range cases {
   649  				req := NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s&skip=%d&take=%d", url, c.Query, c.Skip, c.Take)).
   650  					AddBasicAuth(user.Name)
   651  				resp := MakeRequest(t, req, http.StatusOK)
   652  
   653  				var result nuget.SearchResultResponse
   654  				DecodeJSON(t, resp, &result)
   655  
   656  				assert.Equal(t, c.ExpectedTotal, result.TotalHits, "case %d: unexpected total hits", i)
   657  				assert.Len(t, result.Data, c.ExpectedResults, "case %d: unexpected result count", i)
   658  			}
   659  
   660  			t.Run("EnforceGrouped", func(t *testing.T) {
   661  				defer tests.PrintCurrentTest(t)()
   662  
   663  				req := NewRequestWithBody(t, "PUT", url, createPackage(packageName+".dummy", "1.0.0")).
   664  					AddBasicAuth(user.Name)
   665  				MakeRequest(t, req, http.StatusCreated)
   666  
   667  				req = NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s", url, packageName)).
   668  					AddBasicAuth(user.Name)
   669  				resp := MakeRequest(t, req, http.StatusOK)
   670  
   671  				var result nuget.SearchResultResponse
   672  				DecodeJSON(t, resp, &result)
   673  
   674  				assert.EqualValues(t, 2, result.TotalHits)
   675  				assert.Len(t, result.Data, 2)
   676  				for _, sr := range result.Data {
   677  					if sr.ID == packageName {
   678  						assert.Len(t, sr.Versions, 2)
   679  					} else {
   680  						assert.Len(t, sr.Versions, 1)
   681  					}
   682  				}
   683  
   684  				req = NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName+".dummy", "1.0.0")).
   685  					AddBasicAuth(user.Name)
   686  				MakeRequest(t, req, http.StatusNoContent)
   687  			})
   688  		})
   689  
   690  		for _, fakePackageName := range fakePackages {
   691  			req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, fakePackageName, "1.0.99")).
   692  				AddBasicAuth(user.Name)
   693  			MakeRequest(t, req, http.StatusNoContent)
   694  		}
   695  	})
   696  
   697  	t.Run("RegistrationService", func(t *testing.T) {
   698  		indexURL := fmt.Sprintf("%s%s/registration/%s/index.json", setting.AppURL, url[1:], packageName)
   699  		leafURL := fmt.Sprintf("%s%s/registration/%s/%s.json", setting.AppURL, url[1:], packageName, packageVersion)
   700  		contentURL := fmt.Sprintf("%s%s/package/%s/%s/%s.%s.nupkg", setting.AppURL, url[1:], packageName, packageVersion, packageName, packageVersion)
   701  
   702  		t.Run("RegistrationIndex", func(t *testing.T) {
   703  			defer tests.PrintCurrentTest(t)()
   704  
   705  			req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/index.json", url, packageName)).
   706  				AddBasicAuth(user.Name)
   707  			resp := MakeRequest(t, req, http.StatusOK)
   708  
   709  			var result nuget.RegistrationIndexResponse
   710  			DecodeJSON(t, resp, &result)
   711  
   712  			assert.Equal(t, indexURL, result.RegistrationIndexURL)
   713  			assert.Equal(t, 1, result.Count)
   714  			assert.Len(t, result.Pages, 1)
   715  			assert.Equal(t, indexURL, result.Pages[0].RegistrationPageURL)
   716  			assert.Equal(t, packageVersion, result.Pages[0].Lower)
   717  			assert.Equal(t, packageVersion, result.Pages[0].Upper)
   718  			assert.Equal(t, 1, result.Pages[0].Count)
   719  			assert.Len(t, result.Pages[0].Items, 1)
   720  			assert.Equal(t, packageName, result.Pages[0].Items[0].CatalogEntry.ID)
   721  			assert.Equal(t, packageVersion, result.Pages[0].Items[0].CatalogEntry.Version)
   722  			assert.Equal(t, packageAuthors, result.Pages[0].Items[0].CatalogEntry.Authors)
   723  			assert.Equal(t, packageDescription, result.Pages[0].Items[0].CatalogEntry.Description)
   724  			assert.Equal(t, leafURL, result.Pages[0].Items[0].CatalogEntry.CatalogLeafURL)
   725  			assert.Equal(t, contentURL, result.Pages[0].Items[0].CatalogEntry.PackageContentURL)
   726  		})
   727  
   728  		t.Run("RegistrationLeaf", func(t *testing.T) {
   729  			t.Run("v2", func(t *testing.T) {
   730  				defer tests.PrintCurrentTest(t)()
   731  
   732  				req := NewRequest(t, "GET", fmt.Sprintf("%s/Packages(Id='%s',Version='%s')", url, packageName, packageVersion)).
   733  					AddBasicAuth(user.Name)
   734  				resp := MakeRequest(t, req, http.StatusOK)
   735  
   736  				var result FeedEntry
   737  				decodeXML(t, resp, &result)
   738  
   739  				assert.Equal(t, packageName, result.Properties.Title)
   740  				assert.Equal(t, packageVersion, result.Properties.Version)
   741  				assert.Equal(t, packageAuthors, result.Properties.Authors)
   742  				assert.Equal(t, packageDescription, result.Properties.Description)
   743  				assert.Equal(t, "Microsoft.CSharp:4.5.0:.NETStandard2.0", result.Properties.Dependencies)
   744  			})
   745  
   746  			t.Run("v3", func(t *testing.T) {
   747  				defer tests.PrintCurrentTest(t)()
   748  
   749  				req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/%s.json", url, packageName, packageVersion)).
   750  					AddBasicAuth(user.Name)
   751  				resp := MakeRequest(t, req, http.StatusOK)
   752  
   753  				var result nuget.RegistrationLeafResponse
   754  				DecodeJSON(t, resp, &result)
   755  
   756  				assert.Equal(t, leafURL, result.RegistrationLeafURL)
   757  				assert.Equal(t, contentURL, result.PackageContentURL)
   758  				assert.Equal(t, indexURL, result.RegistrationIndexURL)
   759  			})
   760  		})
   761  	})
   762  
   763  	t.Run("PackageService", func(t *testing.T) {
   764  		t.Run("v2", func(t *testing.T) {
   765  			defer tests.PrintCurrentTest(t)()
   766  
   767  			req := NewRequest(t, "GET", fmt.Sprintf("%s/FindPackagesById()?id='%s'&$top=1", url, packageName)).
   768  				AddBasicAuth(user.Name)
   769  			resp := MakeRequest(t, req, http.StatusOK)
   770  
   771  			var result FeedResponse
   772  			decodeXML(t, resp, &result)
   773  
   774  			assert.Len(t, result.Entries, 1)
   775  			assert.Equal(t, packageVersion, result.Entries[0].Properties.Version)
   776  			assert.Condition(t, containsOneNextLink(t, result.Links))
   777  
   778  			req = NewRequest(t, "GET", fmt.Sprintf("%s/FindPackagesById()/$count?id='%s'", url, packageName)).
   779  				AddBasicAuth(user.Name)
   780  			resp = MakeRequest(t, req, http.StatusOK)
   781  
   782  			assert.Equal(t, "1", resp.Body.String())
   783  		})
   784  
   785  		t.Run("v3", func(t *testing.T) {
   786  			defer tests.PrintCurrentTest(t)()
   787  
   788  			req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/index.json", url, packageName)).
   789  				AddBasicAuth(user.Name)
   790  			resp := MakeRequest(t, req, http.StatusOK)
   791  
   792  			var result nuget.PackageVersionsResponse
   793  			DecodeJSON(t, resp, &result)
   794  
   795  			assert.Len(t, result.Versions, 1)
   796  			assert.Equal(t, packageVersion, result.Versions[0])
   797  		})
   798  	})
   799  
   800  	t.Run("Delete", func(t *testing.T) {
   801  		defer tests.PrintCurrentTest(t)()
   802  
   803  		req := NewRequest(t, "DELETE", fmt.Sprintf("%s/%s/%s", url, packageName, packageVersion)).
   804  			AddBasicAuth(user.Name)
   805  		MakeRequest(t, req, http.StatusNoContent)
   806  
   807  		pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeNuGet)
   808  		assert.NoError(t, err)
   809  		assert.Empty(t, pvs)
   810  	})
   811  
   812  	t.Run("DownloadNotExists", func(t *testing.T) {
   813  		defer tests.PrintCurrentTest(t)()
   814  
   815  		req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", url, packageName, packageVersion, packageName, packageVersion)).
   816  			AddBasicAuth(user.Name)
   817  		MakeRequest(t, req, http.StatusNotFound)
   818  
   819  		req = NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/%s/%s.%s.snupkg", url, packageName, packageVersion, packageName, packageVersion)).
   820  			AddBasicAuth(user.Name)
   821  		MakeRequest(t, req, http.StatusNotFound)
   822  	})
   823  
   824  	t.Run("DeleteNotExists", func(t *testing.T) {
   825  		defer tests.PrintCurrentTest(t)()
   826  
   827  		req := NewRequest(t, "DELETE", fmt.Sprintf("%s/package/%s/%s", url, packageName, packageVersion)).
   828  			AddBasicAuth(user.Name)
   829  		MakeRequest(t, req, http.StatusNotFound)
   830  	})
   831  }