diff --git a/docs/content/doc/packages/nuget.en-us.md b/docs/content/doc/packages/nuget.en-us.md
index 6c8aaa70af..670abca7fd 100644
--- a/docs/content/doc/packages/nuget.en-us.md
+++ b/docs/content/doc/packages/nuget.en-us.md
@@ -14,7 +14,7 @@ menu:
# NuGet Packages Repository
-Publish [NuGet](https://www.nuget.org/) packages for your user or organization. The package registry supports [NuGet Symbol Packages](https://docs.microsoft.com/en-us/nuget/create-packages/symbol-packages-snupkg) too.
+Publish [NuGet](https://www.nuget.org/) packages for your user or organization. The package registry supports the V2 and V3 API protocol and you can work with [NuGet Symbol Packages](https://docs.microsoft.com/en-us/nuget/create-packages/symbol-packages-snupkg) too.
**Table of Contents**
diff --git a/modules/packages/nuget/metadata.go b/modules/packages/nuget/metadata.go
index 797bff45ac..2b555e47e9 100644
--- a/modules/packages/nuget/metadata.go
+++ b/modules/packages/nuget/metadata.go
@@ -55,12 +55,13 @@ type Package struct {
// Metadata represents the metadata of a Nuget package
type Metadata struct {
- Description string `json:"description,omitempty"`
- ReleaseNotes string `json:"release_notes,omitempty"`
- Authors string `json:"authors,omitempty"`
- ProjectURL string `json:"project_url,omitempty"`
- RepositoryURL string `json:"repository_url,omitempty"`
- Dependencies map[string][]Dependency `json:"dependencies,omitempty"`
+ Description string `json:"description,omitempty"`
+ ReleaseNotes string `json:"release_notes,omitempty"`
+ Authors string `json:"authors,omitempty"`
+ ProjectURL string `json:"project_url,omitempty"`
+ RepositoryURL string `json:"repository_url,omitempty"`
+ RequireLicenseAcceptance bool `json:"require_license_acceptance"`
+ Dependencies map[string][]Dependency `json:"dependencies,omitempty"`
}
// Dependency represents a dependency of a Nuget package
@@ -155,12 +156,13 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) {
}
m := &Metadata{
- Description: p.Metadata.Description,
- ReleaseNotes: p.Metadata.ReleaseNotes,
- Authors: p.Metadata.Authors,
- ProjectURL: p.Metadata.ProjectURL,
- RepositoryURL: p.Metadata.Repository.URL,
- Dependencies: make(map[string][]Dependency),
+ Description: p.Metadata.Description,
+ ReleaseNotes: p.Metadata.ReleaseNotes,
+ Authors: p.Metadata.Authors,
+ ProjectURL: p.Metadata.ProjectURL,
+ RepositoryURL: p.Metadata.Repository.URL,
+ RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance,
+ Dependencies: make(map[string][]Dependency),
}
for _, group := range p.Metadata.Dependencies.Group {
diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go
index a54add0621..f6ab961f5e 100644
--- a/routers/api/packages/api.go
+++ b/routers/api/packages/api.go
@@ -180,15 +180,19 @@ func Routes(ctx gocontext.Context) *web.Route {
r.Get("/*", maven.DownloadPackageFile)
}, reqPackageAccess(perm.AccessModeRead))
r.Group("/nuget", func() {
- r.Get("/index.json", nuget.ServiceIndex) // Needs to be unauthenticated for the NuGet client.
+ r.Group("", func() { // Needs to be unauthenticated for the NuGet client.
+ r.Get("/", nuget.ServiceIndexV2)
+ r.Get("/index.json", nuget.ServiceIndexV3)
+ r.Get("/$metadata", nuget.FeedCapabilityResource)
+ })
r.Group("", func() {
- r.Get("/query", nuget.SearchService)
+ r.Get("/query", nuget.SearchServiceV3)
r.Group("/registration/{id}", func() {
r.Get("/index.json", nuget.RegistrationIndex)
- r.Get("/{version}", nuget.RegistrationLeaf)
+ r.Get("/{version}", nuget.RegistrationLeafV3)
})
r.Group("/package/{id}", func() {
- r.Get("/index.json", nuget.EnumeratePackageVersions)
+ r.Get("/index.json", nuget.EnumeratePackageVersionsV3)
r.Get("/{version}/{filename}", nuget.DownloadPackageFile)
})
r.Group("", func() {
@@ -197,6 +201,10 @@ func Routes(ctx gocontext.Context) *web.Route {
r.Delete("/{id}/{version}", nuget.DeletePackage)
}, reqPackageAccess(perm.AccessModeWrite))
r.Get("/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}", nuget.DownloadSymbolFile)
+ r.Get("/Packages(Id='{id:[^']+}',Version='{version:[^']+}')", nuget.RegistrationLeafV2)
+ r.Get("/Packages()", nuget.SearchServiceV2)
+ r.Get("/FindPackagesById()", nuget.EnumeratePackageVersionsV2)
+ r.Get("/Search()", nuget.SearchServiceV2)
}, reqPackageAccess(perm.AccessModeRead))
})
r.Group("/npm", func() {
diff --git a/routers/api/packages/nuget/api_v2.go b/routers/api/packages/nuget/api_v2.go
new file mode 100644
index 0000000000..60a5d9c0e4
--- /dev/null
+++ b/routers/api/packages/nuget/api_v2.go
@@ -0,0 +1,393 @@
+// 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 nuget
+
+import (
+ "encoding/xml"
+ "strings"
+ "time"
+
+ packages_model "code.gitea.io/gitea/models/packages"
+ nuget_module "code.gitea.io/gitea/modules/packages/nuget"
+)
+
+type AtomTitle struct {
+ Type string `xml:"type,attr"`
+ Text string `xml:",chardata"`
+}
+
+type ServiceCollection struct {
+ Href string `xml:"href,attr"`
+ Title AtomTitle `xml:"atom:title"`
+}
+
+type ServiceWorkspace struct {
+ Title AtomTitle `xml:"atom:title"`
+ Collection ServiceCollection `xml:"collection"`
+}
+
+type ServiceIndexResponseV2 struct {
+ XMLName xml.Name `xml:"service"`
+ Base string `xml:"base,attr"`
+ Xmlns string `xml:"xmlns,attr"`
+ XmlnsAtom string `xml:"xmlns:atom,attr"`
+ Workspace ServiceWorkspace `xml:"workspace"`
+}
+
+type EdmxPropertyRef struct {
+ Name string `xml:"Name,attr"`
+}
+
+type EdmxProperty struct {
+ Name string `xml:"Name,attr"`
+ Type string `xml:"Type,attr"`
+ Nullable bool `xml:"Nullable,attr"`
+}
+
+type EdmxEntityType struct {
+ Name string `xml:"Name,attr"`
+ HasStream bool `xml:"m:HasStream,attr"`
+ Keys []EdmxPropertyRef `xml:"Key>PropertyRef"`
+ Properties []EdmxProperty `xml:"Property"`
+}
+
+type EdmxFunctionParameter struct {
+ Name string `xml:"Name,attr"`
+ Type string `xml:"Type,attr"`
+}
+
+type EdmxFunctionImport struct {
+ Name string `xml:"Name,attr"`
+ ReturnType string `xml:"ReturnType,attr"`
+ EntitySet string `xml:"EntitySet,attr"`
+ Parameter []EdmxFunctionParameter `xml:"Parameter"`
+}
+
+type EdmxEntitySet struct {
+ Name string `xml:"Name,attr"`
+ EntityType string `xml:"EntityType,attr"`
+}
+
+type EdmxEntityContainer struct {
+ Name string `xml:"Name,attr"`
+ IsDefaultEntityContainer bool `xml:"m:IsDefaultEntityContainer,attr"`
+ EntitySet EdmxEntitySet `xml:"EntitySet"`
+ FunctionImports []EdmxFunctionImport `xml:"FunctionImport"`
+}
+
+type EdmxSchema struct {
+ Xmlns string `xml:"xmlns,attr"`
+ Namespace string `xml:"Namespace,attr"`
+ EntityType *EdmxEntityType `xml:"EntityType,omitempty"`
+ EntityContainer *EdmxEntityContainer `xml:"EntityContainer,omitempty"`
+}
+
+type EdmxDataServices struct {
+ XmlnsM string `xml:"xmlns:m,attr"`
+ DataServiceVersion string `xml:"m:DataServiceVersion,attr"`
+ MaxDataServiceVersion string `xml:"m:MaxDataServiceVersion,attr"`
+ Schema []EdmxSchema `xml:"Schema"`
+}
+
+type EdmxMetadata struct {
+ XMLName xml.Name `xml:"edmx:Edmx"`
+ XmlnsEdmx string `xml:"xmlns:edmx,attr"`
+ Version string `xml:"Version,attr"`
+ DataServices EdmxDataServices `xml:"edmx:DataServices"`
+}
+
+var Metadata = &EdmxMetadata{
+ XmlnsEdmx: "http://schemas.microsoft.com/ado/2007/06/edmx",
+ Version: "1.0",
+ DataServices: EdmxDataServices{
+ XmlnsM: "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata",
+ DataServiceVersion: "2.0",
+ MaxDataServiceVersion: "2.0",
+ Schema: []EdmxSchema{
+ {
+ Xmlns: "http://schemas.microsoft.com/ado/2006/04/edm",
+ Namespace: "NuGetGallery.OData",
+ EntityType: &EdmxEntityType{
+ Name: "V2FeedPackage",
+ HasStream: true,
+ Keys: []EdmxPropertyRef{
+ {Name: "Id"},
+ {Name: "Version"},
+ },
+ Properties: []EdmxProperty{
+ {
+ Name: "Id",
+ Type: "Edm.String",
+ },
+ {
+ Name: "Version",
+ Type: "Edm.String",
+ },
+ {
+ Name: "NormalizedVersion",
+ Type: "Edm.String",
+ Nullable: true,
+ },
+ {
+ Name: "Authors",
+ Type: "Edm.String",
+ Nullable: true,
+ },
+ {
+ Name: "Created",
+ Type: "Edm.DateTime",
+ },
+ {
+ Name: "Dependencies",
+ Type: "Edm.String",
+ },
+ {
+ Name: "Description",
+ Type: "Edm.String",
+ },
+ {
+ Name: "DownloadCount",
+ Type: "Edm.Int64",
+ },
+ {
+ Name: "LastUpdated",
+ Type: "Edm.DateTime",
+ },
+ {
+ Name: "Published",
+ Type: "Edm.DateTime",
+ },
+ {
+ Name: "PackageSize",
+ Type: "Edm.Int64",
+ },
+ {
+ Name: "ProjectUrl",
+ Type: "Edm.String",
+ Nullable: true,
+ },
+ {
+ Name: "ReleaseNotes",
+ Type: "Edm.String",
+ Nullable: true,
+ },
+ {
+ Name: "RequireLicenseAcceptance",
+ Type: "Edm.Boolean",
+ Nullable: false,
+ },
+ {
+ Name: "Title",
+ Type: "Edm.String",
+ Nullable: true,
+ },
+ {
+ Name: "VersionDownloadCount",
+ Type: "Edm.Int64",
+ Nullable: false,
+ },
+ },
+ },
+ },
+ {
+ Xmlns: "http://schemas.microsoft.com/ado/2006/04/edm",
+ Namespace: "NuGetGallery",
+ EntityContainer: &EdmxEntityContainer{
+ Name: "V2FeedContext",
+ IsDefaultEntityContainer: true,
+ EntitySet: EdmxEntitySet{
+ Name: "Packages",
+ EntityType: "NuGetGallery.OData.V2FeedPackage",
+ },
+ FunctionImports: []EdmxFunctionImport{
+ {
+ Name: "Search",
+ ReturnType: "Collection(NuGetGallery.OData.V2FeedPackage)",
+ EntitySet: "Packages",
+ Parameter: []EdmxFunctionParameter{
+ {
+ Name: "searchTerm",
+ Type: "Edm.String",
+ },
+ },
+ },
+ {
+ Name: "FindPackagesById",
+ ReturnType: "Collection(NuGetGallery.OData.V2FeedPackage)",
+ EntitySet: "Packages",
+ Parameter: []EdmxFunctionParameter{
+ {
+ Name: "id",
+ Type: "Edm.String",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+}
+
+type FeedEntryCategory struct {
+ Term string `xml:"term,attr"`
+ Scheme string `xml:"scheme,attr"`
+}
+
+type FeedEntryLink struct {
+ Rel string `xml:"rel,attr"`
+ Href string `xml:"href,attr"`
+}
+
+type TypedValue[T any] struct {
+ Type string `xml:"type,attr,omitempty"`
+ Value T `xml:",chardata"`
+}
+
+type FeedEntryProperties struct {
+ Version string `xml:"d:Version"`
+ NormalizedVersion string `xml:"d:NormalizedVersion"`
+ Authors string `xml:"d:Authors"`
+ Dependencies string `xml:"d:Dependencies"`
+ Description string `xml:"d:Description"`
+ VersionDownloadCount TypedValue[int64] `xml:"d:VersionDownloadCount"`
+ DownloadCount TypedValue[int64] `xml:"d:DownloadCount"`
+ PackageSize TypedValue[int64] `xml:"d:PackageSize"`
+ Created TypedValue[time.Time] `xml:"d:Created"`
+ LastUpdated TypedValue[time.Time] `xml:"d:LastUpdated"`
+ Published TypedValue[time.Time] `xml:"d:Published"`
+ ProjectURL string `xml:"d:ProjectUrl,omitempty"`
+ ReleaseNotes string `xml:"d:ReleaseNotes,omitempty"`
+ RequireLicenseAcceptance TypedValue[bool] `xml:"d:RequireLicenseAcceptance"`
+ Title string `xml:"d:Title"`
+}
+
+type FeedEntry struct {
+ XMLName xml.Name `xml:"entry"`
+ Xmlns string `xml:"xmlns,attr,omitempty"`
+ XmlnsD string `xml:"xmlns:d,attr,omitempty"`
+ XmlnsM string `xml:"xmlns:m,attr,omitempty"`
+ Base string `xml:"xml:base,attr,omitempty"`
+ ID string `xml:"id"`
+ Category FeedEntryCategory `xml:"category"`
+ Links []FeedEntryLink `xml:"link"`
+ Title TypedValue[string] `xml:"title"`
+ Updated time.Time `xml:"updated"`
+ Author string `xml:"author>name"`
+ Summary string `xml:"summary"`
+ Properties *FeedEntryProperties `xml:"m:properties"`
+ Content string `xml:",innerxml"`
+}
+
+type FeedResponse struct {
+ XMLName xml.Name `xml:"feed"`
+ Xmlns string `xml:"xmlns,attr,omitempty"`
+ XmlnsD string `xml:"xmlns:d,attr,omitempty"`
+ XmlnsM string `xml:"xmlns:m,attr,omitempty"`
+ Base string `xml:"xml:base,attr,omitempty"`
+ ID string `xml:"id"`
+ Title TypedValue[string] `xml:"title"`
+ Updated time.Time `xml:"updated"`
+ Link FeedEntryLink `xml:"link"`
+ Entries []*FeedEntry `xml:"entry"`
+ Count int64 `xml:"m:count"`
+}
+
+func createFeedResponse(l *linkBuilder, totalEntries int64, pds []*packages_model.PackageDescriptor) *FeedResponse {
+ entries := make([]*FeedEntry, 0, len(pds))
+ for _, pd := range pds {
+ entries = append(entries, createEntry(l, pd, false))
+ }
+
+ return &FeedResponse{
+ Xmlns: "http://www.w3.org/2005/Atom",
+ Base: l.Base,
+ XmlnsD: "http://schemas.microsoft.com/ado/2007/08/dataservices",
+ XmlnsM: "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata",
+ ID: "http://schemas.datacontract.org/2004/07/",
+ Updated: time.Now(),
+ Link: FeedEntryLink{Rel: "self", Href: l.Base},
+ Count: totalEntries,
+ Entries: entries,
+ }
+}
+
+func createEntryResponse(l *linkBuilder, pd *packages_model.PackageDescriptor) *FeedEntry {
+ return createEntry(l, pd, true)
+}
+
+func createEntry(l *linkBuilder, pd *packages_model.PackageDescriptor, withNamespace bool) *FeedEntry {
+ metadata := pd.Metadata.(*nuget_module.Metadata)
+
+ id := l.GetPackageMetadataURL(pd.Package.Name, pd.Version.Version)
+
+ // Workaround to force a self-closing tag to satisfy XmlReader.IsEmptyElement used by the NuGet client.
+ // https://learn.microsoft.com/en-us/dotnet/api/system.xml.xmlreader.isemptyelement
+ content := ``
+
+ createdValue := TypedValue[time.Time]{
+ Type: "Edm.DateTime",
+ Value: pd.Version.CreatedUnix.AsLocalTime(),
+ }
+
+ entry := &FeedEntry{
+ ID: id,
+ Category: FeedEntryCategory{Term: "NuGetGallery.OData.V2FeedPackage", Scheme: "http://schemas.microsoft.com/ado/2007/08/dataservices/scheme"},
+ Links: []FeedEntryLink{
+ {Rel: "self", Href: id},
+ {Rel: "edit", Href: id},
+ },
+ Title: TypedValue[string]{Type: "text", Value: pd.Package.Name},
+ Updated: pd.Version.CreatedUnix.AsLocalTime(),
+ Author: metadata.Authors,
+ Content: content,
+ Properties: &FeedEntryProperties{
+ Version: pd.Version.Version,
+ NormalizedVersion: normalizeVersion(pd.SemVer),
+ Authors: metadata.Authors,
+ Dependencies: buildDependencyString(metadata),
+ Description: metadata.Description,
+ VersionDownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount},
+ DownloadCount: TypedValue[int64]{Type: "Edm.Int64", Value: pd.Version.DownloadCount},
+ PackageSize: TypedValue[int64]{Type: "Edm.Int64", Value: pd.CalculateBlobSize()},
+ Created: createdValue,
+ LastUpdated: createdValue,
+ Published: createdValue,
+ ProjectURL: metadata.ProjectURL,
+ ReleaseNotes: metadata.ReleaseNotes,
+ RequireLicenseAcceptance: TypedValue[bool]{Type: "Edm.Boolean", Value: metadata.RequireLicenseAcceptance},
+ Title: pd.Package.Name,
+ },
+ }
+
+ if withNamespace {
+ entry.Xmlns = "http://www.w3.org/2005/Atom"
+ entry.Base = l.Base
+ entry.XmlnsD = "http://schemas.microsoft.com/ado/2007/08/dataservices"
+ entry.XmlnsM = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"
+ }
+
+ return entry
+}
+
+func buildDependencyString(metadata *nuget_module.Metadata) string {
+ var b strings.Builder
+ first := true
+ for group, deps := range metadata.Dependencies {
+ for _, dep := range deps {
+ if !first {
+ b.WriteByte('|')
+ }
+ first = false
+
+ b.WriteString(dep.ID)
+ b.WriteByte(':')
+ b.WriteString(dep.Version)
+ b.WriteByte(':')
+ b.WriteString(group)
+ }
+ }
+ return b.String()
+}
diff --git a/routers/api/packages/nuget/api.go b/routers/api/packages/nuget/api_v3.go
similarity index 79%
rename from routers/api/packages/nuget/api.go
rename to routers/api/packages/nuget/api_v3.go
index 964e05f926..552054f26b 100644
--- a/routers/api/packages/nuget/api.go
+++ b/routers/api/packages/nuget/api_v3.go
@@ -16,36 +16,19 @@ import (
"github.com/hashicorp/go-version"
)
-// ServiceIndexResponse https://docs.microsoft.com/en-us/nuget/api/service-index#resources
-type ServiceIndexResponse struct {
+// https://docs.microsoft.com/en-us/nuget/api/service-index#resources
+type ServiceIndexResponseV3 struct {
Version string `json:"version"`
Resources []ServiceResource `json:"resources"`
}
-// ServiceResource https://docs.microsoft.com/en-us/nuget/api/service-index#resource
+// https://docs.microsoft.com/en-us/nuget/api/service-index#resource
type ServiceResource struct {
ID string `json:"@id"`
Type string `json:"@type"`
}
-func createServiceIndexResponse(root string) *ServiceIndexResponse {
- return &ServiceIndexResponse{
- Version: "3.0.0",
- Resources: []ServiceResource{
- {ID: root + "/query", Type: "SearchQueryService"},
- {ID: root + "/query", Type: "SearchQueryService/3.0.0-beta"},
- {ID: root + "/query", Type: "SearchQueryService/3.0.0-rc"},
- {ID: root + "/registration", Type: "RegistrationsBaseUrl"},
- {ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-beta"},
- {ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-rc"},
- {ID: root + "/package", Type: "PackageBaseAddress/3.0.0"},
- {ID: root, Type: "PackagePublish/2.0.0"},
- {ID: root + "/symbolpackage", Type: "SymbolPackagePublish/4.9.0"},
- },
- }
-}
-
-// RegistrationIndexResponse https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#response
+// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#response
type RegistrationIndexResponse struct {
RegistrationIndexURL string `json:"@id"`
Type []string `json:"@type"`
@@ -53,7 +36,7 @@ type RegistrationIndexResponse struct {
Pages []*RegistrationIndexPage `json:"items"`
}
-// RegistrationIndexPage https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-page-object
+// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-page-object
type RegistrationIndexPage struct {
RegistrationPageURL string `json:"@id"`
Lower string `json:"lower"`
@@ -62,14 +45,14 @@ type RegistrationIndexPage struct {
Items []*RegistrationIndexPageItem `json:"items"`
}
-// RegistrationIndexPageItem https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf-object-in-a-page
+// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf-object-in-a-page
type RegistrationIndexPageItem struct {
RegistrationLeafURL string `json:"@id"`
PackageContentURL string `json:"packageContent"`
CatalogEntry *CatalogEntry `json:"catalogEntry"`
}
-// CatalogEntry https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry
+// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#catalog-entry
type CatalogEntry struct {
CatalogLeafURL string `json:"@id"`
PackageContentURL string `json:"packageContent"`
@@ -83,13 +66,13 @@ type CatalogEntry struct {
DependencyGroups []*PackageDependencyGroup `json:"dependencyGroups"`
}
-// PackageDependencyGroup https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency-group
+// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency-group
type PackageDependencyGroup struct {
TargetFramework string `json:"targetFramework"`
Dependencies []*PackageDependency `json:"dependencies"`
}
-// PackageDependency https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency
+// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency
type PackageDependency struct {
ID string `json:"id"`
Range string `json:"range"`
@@ -162,7 +145,7 @@ func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDepe
return dependencyGroups
}
-// RegistrationLeafResponse https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf
+// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf
type RegistrationLeafResponse struct {
RegistrationLeafURL string `json:"@id"`
Type []string `json:"@type"`
@@ -183,7 +166,7 @@ func createRegistrationLeafResponse(l *linkBuilder, pd *packages_model.PackageDe
}
}
-// PackageVersionsResponse https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#response
+// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#response
type PackageVersionsResponse struct {
Versions []string `json:"versions"`
}
@@ -199,13 +182,13 @@ func createPackageVersionsResponse(pds []*packages_model.PackageDescriptor) *Pac
}
}
-// SearchResultResponse https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#response
+// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#response
type SearchResultResponse struct {
TotalHits int64 `json:"totalHits"`
Data []*SearchResult `json:"data"`
}
-// SearchResult https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
+// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
type SearchResult struct {
ID string `json:"id"`
Version string `json:"version"`
@@ -216,7 +199,7 @@ type SearchResult struct {
RegistrationIndexURL string `json:"registration"`
}
-// SearchResultVersion https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
+// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-result
type SearchResultVersion struct {
RegistrationLeafURL string `json:"@id"`
Version string `json:"version"`
diff --git a/routers/api/packages/nuget/links.go b/routers/api/packages/nuget/links.go
index f782c7f2cb..618b54ae8d 100644
--- a/routers/api/packages/nuget/links.go
+++ b/routers/api/packages/nuget/links.go
@@ -26,3 +26,8 @@ func (l *linkBuilder) GetRegistrationLeafURL(id, version string) string {
func (l *linkBuilder) GetPackageDownloadURL(id, version string) string {
return fmt.Sprintf("%s/package/%s/%s/%s.%s.nupkg", l.Base, id, version, id, version)
}
+
+// GetPackageMetadataURL builds the package metadata url
+func (l *linkBuilder) GetPackageMetadataURL(id, version string) string {
+ return fmt.Sprintf("%s/Packages(Id='%s',Version='%s')", l.Base, id, version)
+}
diff --git a/routers/api/packages/nuget/nuget.go b/routers/api/packages/nuget/nuget.go
index 3c61ae28bb..e84aef3160 100644
--- a/routers/api/packages/nuget/nuget.go
+++ b/routers/api/packages/nuget/nuget.go
@@ -5,15 +5,18 @@
package nuget
import (
+ "encoding/xml"
"errors"
"fmt"
"io"
"net/http"
+ "regexp"
"strings"
"code.gitea.io/gitea/models/db"
packages_model "code.gitea.io/gitea/models/packages"
"code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
packages_module "code.gitea.io/gitea/modules/packages"
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
"code.gitea.io/gitea/modules/setting"
@@ -30,15 +33,121 @@ func apiError(ctx *context.Context, status int, obj interface{}) {
})
}
-// ServiceIndex https://docs.microsoft.com/en-us/nuget/api/service-index
-func ServiceIndex(ctx *context.Context) {
- resp := createServiceIndexResponse(setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget")
-
- ctx.JSON(http.StatusOK, resp)
+func xmlResponse(ctx *context.Context, status int, obj interface{}) {
+ ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8")
+ ctx.Resp.WriteHeader(status)
+ if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil {
+ log.Error("Write failed: %v", err)
+ }
+ if err := xml.NewEncoder(ctx.Resp).Encode(obj); err != nil {
+ log.Error("XML encode failed: %v", err)
+ }
}
-// SearchService https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
-func SearchService(ctx *context.Context) {
+// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
+func ServiceIndexV2(ctx *context.Context) {
+ base := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"
+
+ xmlResponse(ctx, http.StatusOK, &ServiceIndexResponseV2{
+ Base: base,
+ Xmlns: "http://www.w3.org/2007/app",
+ XmlnsAtom: "http://www.w3.org/2005/Atom",
+ Workspace: ServiceWorkspace{
+ Title: AtomTitle{
+ Type: "text",
+ Text: "Default",
+ },
+ Collection: ServiceCollection{
+ Href: "Packages",
+ Title: AtomTitle{
+ Type: "text",
+ Text: "Packages",
+ },
+ },
+ },
+ })
+}
+
+// https://docs.microsoft.com/en-us/nuget/api/service-index
+func ServiceIndexV3(ctx *context.Context) {
+ root := setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"
+
+ ctx.JSON(http.StatusOK, &ServiceIndexResponseV3{
+ Version: "3.0.0",
+ Resources: []ServiceResource{
+ {ID: root + "/query", Type: "SearchQueryService"},
+ {ID: root + "/query", Type: "SearchQueryService/3.0.0-beta"},
+ {ID: root + "/query", Type: "SearchQueryService/3.0.0-rc"},
+ {ID: root + "/registration", Type: "RegistrationsBaseUrl"},
+ {ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-beta"},
+ {ID: root + "/registration", Type: "RegistrationsBaseUrl/3.0.0-rc"},
+ {ID: root + "/package", Type: "PackageBaseAddress/3.0.0"},
+ {ID: root, Type: "PackagePublish/2.0.0"},
+ {ID: root + "/symbolpackage", Type: "SymbolPackagePublish/4.9.0"},
+ },
+ })
+}
+
+// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/LegacyFeedCapabilityResourceV2Feed.cs
+func FeedCapabilityResource(ctx *context.Context) {
+ xmlResponse(ctx, http.StatusOK, Metadata)
+}
+
+var searchTermExtract = regexp.MustCompile(`'([^']+)'`)
+
+// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
+func SearchServiceV2(ctx *context.Context) {
+ searchTerm := strings.Trim(ctx.FormTrim("searchTerm"), "'")
+ if searchTerm == "" {
+ // $filter contains a query like:
+ // (((Id ne null) and substringof('microsoft',tolower(Id)))
+ // We don't support these queries, just extract the search term.
+ match := searchTermExtract.FindStringSubmatch(ctx.FormTrim("$filter"))
+ if len(match) == 2 {
+ searchTerm = strings.TrimSpace(match[1])
+ }
+ }
+
+ skip, take := ctx.FormInt("skip"), ctx.FormInt("take")
+ if skip == 0 {
+ skip = ctx.FormInt("$skip")
+ }
+ if take == 0 {
+ take = ctx.FormInt("$top")
+ }
+
+ pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
+ OwnerID: ctx.Package.Owner.ID,
+ Type: packages_model.TypeNuGet,
+ Name: packages_model.SearchValue{Value: searchTerm},
+ IsInternal: util.OptionalBoolFalse,
+ Paginator: db.NewAbsoluteListOptions(
+ skip,
+ take,
+ ),
+ })
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ resp := createFeedResponse(
+ &linkBuilder{setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
+ total,
+ pds,
+ )
+
+ xmlResponse(ctx, http.StatusOK, resp)
+}
+
+// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
+func SearchServiceV3(ctx *context.Context) {
pvs, count, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
OwnerID: ctx.Package.Owner.ID,
Type: packages_model.TypeNuGet,
@@ -69,7 +178,7 @@ func SearchService(ctx *context.Context) {
ctx.JSON(http.StatusOK, resp)
}
-// RegistrationIndex https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-index
+// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-index
func RegistrationIndex(ctx *context.Context) {
packageName := ctx.Params("id")
@@ -97,8 +206,37 @@ func RegistrationIndex(ctx *context.Context) {
ctx.JSON(http.StatusOK, resp)
}
-// RegistrationLeaf https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf
-func RegistrationLeaf(ctx *context.Context) {
+// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
+func RegistrationLeafV2(ctx *context.Context) {
+ packageName := ctx.Params("id")
+ packageVersion := ctx.Params("version")
+
+ pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName, packageVersion)
+ if err != nil {
+ if err == packages_model.ErrPackageNotExist {
+ apiError(ctx, http.StatusNotFound, err)
+ return
+ }
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ pd, err := packages_model.GetPackageDescriptor(ctx, pv)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ resp := createEntryResponse(
+ &linkBuilder{setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
+ pd,
+ )
+
+ xmlResponse(ctx, http.StatusOK, resp)
+}
+
+// https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf
+func RegistrationLeafV3(ctx *context.Context) {
packageName := ctx.Params("id")
packageVersion := strings.TrimSuffix(ctx.Params("version"), ".json")
@@ -126,8 +264,33 @@ func RegistrationLeaf(ctx *context.Context) {
ctx.JSON(http.StatusOK, resp)
}
-// EnumeratePackageVersions https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions
-func EnumeratePackageVersions(ctx *context.Context) {
+// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
+func EnumeratePackageVersionsV2(ctx *context.Context) {
+ packageName := strings.Trim(ctx.FormTrim("id"), "'")
+
+ pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ pds, err := packages_model.GetPackageDescriptors(ctx, pvs)
+ if err != nil {
+ apiError(ctx, http.StatusInternalServerError, err)
+ return
+ }
+
+ resp := createFeedResponse(
+ &linkBuilder{setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget"},
+ int64(len(pds)),
+ pds,
+ )
+
+ xmlResponse(ctx, http.StatusOK, resp)
+}
+
+// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions
+func EnumeratePackageVersionsV3(ctx *context.Context) {
packageName := ctx.Params("id")
pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName)
@@ -151,7 +314,7 @@ func EnumeratePackageVersions(ctx *context.Context) {
ctx.JSON(http.StatusOK, resp)
}
-// DownloadPackageFile https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
+// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#download-package-content-nupkg
func DownloadPackageFile(ctx *context.Context) {
packageName := ctx.Params("id")
packageVersion := ctx.Params("version")
@@ -350,7 +513,7 @@ func processUploadedFile(ctx *context.Context, expectedType nuget_module.Package
return np, buf, closables
}
-// DownloadSymbolFile https://github.com/dotnet/symstore/blob/main/docs/specs/Simple_Symbol_Query_Protocol.md#request
+// https://github.com/dotnet/symstore/blob/main/docs/specs/Simple_Symbol_Query_Protocol.md#request
func DownloadSymbolFile(ctx *context.Context) {
filename := ctx.Params("filename")
guid := ctx.Params("guid")[:32]
diff --git a/tests/integration/api_packages_nuget_test.go b/tests/integration/api_packages_nuget_test.go
index 8d5a5c7c82..f1f8a950c6 100644
--- a/tests/integration/api_packages_nuget_test.go
+++ b/tests/integration/api_packages_nuget_test.go
@@ -8,10 +8,13 @@ import (
"archive/zip"
"bytes"
"encoding/base64"
+ "encoding/xml"
"fmt"
"io"
"net/http"
+ "net/http/httptest"
"testing"
+ "time"
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/models/packages"
@@ -31,9 +34,45 @@ func addNuGetAPIKeyHeader(request *http.Request, token string) *http.Request {
return request
}
+func decodeXML(t testing.TB, resp *httptest.ResponseRecorder, v interface{}) {
+ t.Helper()
+
+ assert.NoError(t, xml.NewDecoder(resp.Body).Decode(v))
+}
+
func TestPackageNuGet(t *testing.T) {
defer tests.PrepareTestEnv(t)()
+ type FeedEntryProperties struct {
+ Version string `xml:"Version"`
+ NormalizedVersion string `xml:"NormalizedVersion"`
+ Authors string `xml:"Authors"`
+ Dependencies string `xml:"Dependencies"`
+ Description string `xml:"Description"`
+ VersionDownloadCount nuget.TypedValue[int64] `xml:"VersionDownloadCount"`
+ DownloadCount nuget.TypedValue[int64] `xml:"DownloadCount"`
+ PackageSize nuget.TypedValue[int64] `xml:"PackageSize"`
+ Created nuget.TypedValue[time.Time] `xml:"Created"`
+ LastUpdated nuget.TypedValue[time.Time] `xml:"LastUpdated"`
+ Published nuget.TypedValue[time.Time] `xml:"Published"`
+ ProjectURL string `xml:"ProjectUrl,omitempty"`
+ ReleaseNotes string `xml:"ReleaseNotes,omitempty"`
+ RequireLicenseAcceptance nuget.TypedValue[bool] `xml:"RequireLicenseAcceptance"`
+ Title string `xml:"Title"`
+ }
+
+ type FeedEntry struct {
+ XMLName xml.Name `xml:"entry"`
+ Properties *FeedEntryProperties `xml:"properties"`
+ Content string `xml:",innerxml"`
+ }
+
+ type FeedResponse struct {
+ XMLName xml.Name `xml:"feed"`
+ Entries []*FeedEntry `xml:"entry"`
+ Count int64 `xml:"count"`
+ }
+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user.Name)
@@ -54,9 +93,11 @@ func TestPackageNuGet(t *testing.T) {
` + packageVersion + `
` + packageAuthors + `
` + packageDescription + `
-
-
-
+
+
+
+
+
`))
archive.Close()
@@ -67,60 +108,101 @@ func TestPackageNuGet(t *testing.T) {
t.Run("ServiceIndex", func(t *testing.T) {
defer tests.PrintCurrentTest(t)()
- privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate})
+ t.Run("v2", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
- cases := []struct {
- Owner string
- UseBasicAuth bool
- UseTokenAuth bool
- }{
- {privateUser.Name, false, false},
- {privateUser.Name, true, false},
- {privateUser.Name, false, true},
- {user.Name, false, false},
- {user.Name, true, false},
- {user.Name, false, true},
- }
+ privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate})
- for _, c := range cases {
- url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner)
-
- req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
- if c.UseBasicAuth {
- req = AddBasicAuthHeader(req, user.Name)
- } else if c.UseTokenAuth {
- req = addNuGetAPIKeyHeader(req, token)
+ cases := []struct {
+ Owner string
+ UseBasicAuth bool
+ UseTokenAuth bool
+ }{
+ {privateUser.Name, false, false},
+ {privateUser.Name, true, false},
+ {privateUser.Name, false, true},
+ {user.Name, false, false},
+ {user.Name, true, false},
+ {user.Name, false, true},
}
- resp := MakeRequest(t, req, http.StatusOK)
- var result nuget.ServiceIndexResponse
- DecodeJSON(t, resp, &result)
+ for _, c := range cases {
+ url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner)
- assert.Equal(t, "3.0.0", result.Version)
- assert.NotEmpty(t, result.Resources)
+ req := NewRequest(t, "GET", url)
+ if c.UseBasicAuth {
+ req = AddBasicAuthHeader(req, user.Name)
+ } else if c.UseTokenAuth {
+ req = addNuGetAPIKeyHeader(req, token)
+ }
+ resp := MakeRequest(t, req, http.StatusOK)
- 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)
+ var result nuget.ServiceIndexResponseV2
+ decodeXML(t, resp, &result)
+
+ assert.Equal(t, setting.AppURL+url[1:], result.Base)
+ assert.Equal(t, "Packages", result.Workspace.Collection.Href)
+ }
+ })
+
+ t.Run("v3", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate})
+
+ cases := []struct {
+ Owner string
+ UseBasicAuth bool
+ UseTokenAuth bool
+ }{
+ {privateUser.Name, false, false},
+ {privateUser.Name, true, false},
+ {privateUser.Name, false, true},
+ {user.Name, false, false},
+ {user.Name, true, false},
+ {user.Name, false, true},
+ }
+
+ for _, c := range cases {
+ url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner)
+
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url))
+ if c.UseBasicAuth {
+ req = AddBasicAuthHeader(req, user.Name)
+ } else if c.UseTokenAuth {
+ req = addNuGetAPIKeyHeader(req, token)
+ }
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result nuget.ServiceIndexResponseV3
+ 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) {
@@ -305,17 +387,57 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
{"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)
+ t.Run("v2", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
- var result nuget.SearchResultResponse
- DecodeJSON(t, resp, &result)
+ t.Run("Search()", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
- 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)
- }
+ for i, c := range cases {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/Search()?searchTerm='%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 FeedResponse
+ decodeXML(t, resp, &result)
+
+ assert.Equal(t, c.ExpectedTotal, result.Count, "case %d: unexpected total hits", i)
+ assert.Len(t, result.Entries, c.ExpectedResults, "case %d: unexpected result count", i)
+ }
+ })
+
+ t.Run("Packages()", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ for i, c := range cases {
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/Search()?$filter=substringof('%s',tolower(Id))&$skip=%d&$top=%d", url, c.Query, c.Skip, c.Take))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
+
+ var result FeedResponse
+ decodeXML(t, resp, &result)
+
+ assert.Equal(t, c.ExpectedTotal, result.Count, "case %d: unexpected total hits", i)
+ assert.Len(t, result.Entries, c.ExpectedResults, "case %d: unexpected result count", i)
+ }
+ })
+ })
+
+ t.Run("v3", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
+
+ 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) {
@@ -352,31 +474,70 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`)
t.Run("RegistrationLeaf", func(t *testing.T) {
defer tests.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)
+ t.Run("v2", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
- var result nuget.RegistrationLeafResponse
- DecodeJSON(t, resp, &result)
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/Packages(Id='%s',Version='%s')", url, packageName, packageVersion))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
- assert.Equal(t, leafURL, result.RegistrationLeafURL)
- assert.Equal(t, contentURL, result.PackageContentURL)
- assert.Equal(t, indexURL, result.RegistrationIndexURL)
+ var result FeedEntry
+ decodeXML(t, resp, &result)
+
+ assert.Equal(t, packageName, result.Properties.Title)
+ assert.Equal(t, packageVersion, result.Properties.Version)
+ assert.Equal(t, packageAuthors, result.Properties.Authors)
+ assert.Equal(t, packageDescription, result.Properties.Description)
+ assert.Equal(t, "Microsoft.CSharp:4.5.0:.NETStandard2.0", result.Properties.Dependencies)
+ })
+
+ t.Run("v3", func(t *testing.T) {
+ defer tests.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 tests.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)
+ t.Run("v2", func(t *testing.T) {
+ defer tests.PrintCurrentTest(t)()
- var result nuget.PackageVersionsResponse
- DecodeJSON(t, resp, &result)
+ req := NewRequest(t, "GET", fmt.Sprintf("%s/FindPackagesById()?id='%s'", url, packageName))
+ req = AddBasicAuthHeader(req, user.Name)
+ resp := MakeRequest(t, req, http.StatusOK)
- assert.Len(t, result.Versions, 1)
- assert.Equal(t, packageVersion, result.Versions[0])
+ var result FeedResponse
+ decodeXML(t, resp, &result)
+
+ assert.Len(t, result.Entries, 1)
+ assert.Equal(t, packageVersion, result.Entries[0].Properties.Version)
+ })
+
+ t.Run("v3", func(t *testing.T) {
+ defer tests.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) {