mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	Add support for Chocolatey/NuGet v2 API (#21393)
Fixes #21294 This PR adds support for NuGet v2 API. Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -14,7 +14,7 @@ menu: | |||||||
|  |  | ||||||
| # NuGet Packages Repository | # 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** | **Table of Contents** | ||||||
|  |  | ||||||
|   | |||||||
| @@ -55,12 +55,13 @@ type Package struct { | |||||||
|  |  | ||||||
| // Metadata represents the metadata of a Nuget package | // Metadata represents the metadata of a Nuget package | ||||||
| type Metadata struct { | type Metadata struct { | ||||||
| 	Description   string                  `json:"description,omitempty"` | 	Description              string                  `json:"description,omitempty"` | ||||||
| 	ReleaseNotes  string                  `json:"release_notes,omitempty"` | 	ReleaseNotes             string                  `json:"release_notes,omitempty"` | ||||||
| 	Authors       string                  `json:"authors,omitempty"` | 	Authors                  string                  `json:"authors,omitempty"` | ||||||
| 	ProjectURL    string                  `json:"project_url,omitempty"` | 	ProjectURL               string                  `json:"project_url,omitempty"` | ||||||
| 	RepositoryURL string                  `json:"repository_url,omitempty"` | 	RepositoryURL            string                  `json:"repository_url,omitempty"` | ||||||
| 	Dependencies  map[string][]Dependency `json:"dependencies,omitempty"` | 	RequireLicenseAcceptance bool                    `json:"require_license_acceptance"` | ||||||
|  | 	Dependencies             map[string][]Dependency `json:"dependencies,omitempty"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Dependency represents a dependency of a Nuget package | // Dependency represents a dependency of a Nuget package | ||||||
| @@ -155,12 +156,13 @@ func ParseNuspecMetaData(r io.Reader) (*Package, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	m := &Metadata{ | 	m := &Metadata{ | ||||||
| 		Description:   p.Metadata.Description, | 		Description:              p.Metadata.Description, | ||||||
| 		ReleaseNotes:  p.Metadata.ReleaseNotes, | 		ReleaseNotes:             p.Metadata.ReleaseNotes, | ||||||
| 		Authors:       p.Metadata.Authors, | 		Authors:                  p.Metadata.Authors, | ||||||
| 		ProjectURL:    p.Metadata.ProjectURL, | 		ProjectURL:               p.Metadata.ProjectURL, | ||||||
| 		RepositoryURL: p.Metadata.Repository.URL, | 		RepositoryURL:            p.Metadata.Repository.URL, | ||||||
| 		Dependencies:  make(map[string][]Dependency), | 		RequireLicenseAcceptance: p.Metadata.RequireLicenseAcceptance, | ||||||
|  | 		Dependencies:             make(map[string][]Dependency), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, group := range p.Metadata.Dependencies.Group { | 	for _, group := range p.Metadata.Dependencies.Group { | ||||||
|   | |||||||
| @@ -180,15 +180,19 @@ func Routes(ctx gocontext.Context) *web.Route { | |||||||
| 			r.Get("/*", maven.DownloadPackageFile) | 			r.Get("/*", maven.DownloadPackageFile) | ||||||
| 		}, reqPackageAccess(perm.AccessModeRead)) | 		}, reqPackageAccess(perm.AccessModeRead)) | ||||||
| 		r.Group("/nuget", func() { | 		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.Group("", func() { | ||||||
| 				r.Get("/query", nuget.SearchService) | 				r.Get("/query", nuget.SearchServiceV3) | ||||||
| 				r.Group("/registration/{id}", func() { | 				r.Group("/registration/{id}", func() { | ||||||
| 					r.Get("/index.json", nuget.RegistrationIndex) | 					r.Get("/index.json", nuget.RegistrationIndex) | ||||||
| 					r.Get("/{version}", nuget.RegistrationLeaf) | 					r.Get("/{version}", nuget.RegistrationLeafV3) | ||||||
| 				}) | 				}) | ||||||
| 				r.Group("/package/{id}", func() { | 				r.Group("/package/{id}", func() { | ||||||
| 					r.Get("/index.json", nuget.EnumeratePackageVersions) | 					r.Get("/index.json", nuget.EnumeratePackageVersionsV3) | ||||||
| 					r.Get("/{version}/{filename}", nuget.DownloadPackageFile) | 					r.Get("/{version}/{filename}", nuget.DownloadPackageFile) | ||||||
| 				}) | 				}) | ||||||
| 				r.Group("", func() { | 				r.Group("", func() { | ||||||
| @@ -197,6 +201,10 @@ func Routes(ctx gocontext.Context) *web.Route { | |||||||
| 					r.Delete("/{id}/{version}", nuget.DeletePackage) | 					r.Delete("/{id}/{version}", nuget.DeletePackage) | ||||||
| 				}, reqPackageAccess(perm.AccessModeWrite)) | 				}, reqPackageAccess(perm.AccessModeWrite)) | ||||||
| 				r.Get("/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}", nuget.DownloadSymbolFile) | 				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)) | 			}, reqPackageAccess(perm.AccessModeRead)) | ||||||
| 		}) | 		}) | ||||||
| 		r.Group("/npm", func() { | 		r.Group("/npm", func() { | ||||||
|   | |||||||
							
								
								
									
										393
									
								
								routers/api/packages/nuget/api_v2.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										393
									
								
								routers/api/packages/nuget/api_v2.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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 := `<content type="application/zip" src="` + l.GetPackageDownloadURL(pd.Package.Name, pd.Version.Version) + `"/>` | ||||||
|  |  | ||||||
|  | 	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() | ||||||
|  | } | ||||||
| @@ -16,36 +16,19 @@ import ( | |||||||
| 	"github.com/hashicorp/go-version" | 	"github.com/hashicorp/go-version" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // ServiceIndexResponse https://docs.microsoft.com/en-us/nuget/api/service-index#resources | // https://docs.microsoft.com/en-us/nuget/api/service-index#resources | ||||||
| type ServiceIndexResponse struct { | type ServiceIndexResponseV3 struct { | ||||||
| 	Version   string            `json:"version"` | 	Version   string            `json:"version"` | ||||||
| 	Resources []ServiceResource `json:"resources"` | 	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 { | type ServiceResource struct { | ||||||
| 	ID   string `json:"@id"` | 	ID   string `json:"@id"` | ||||||
| 	Type string `json:"@type"` | 	Type string `json:"@type"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func createServiceIndexResponse(root string) *ServiceIndexResponse { | // https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#response | ||||||
| 	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 |  | ||||||
| type RegistrationIndexResponse struct { | type RegistrationIndexResponse struct { | ||||||
| 	RegistrationIndexURL string                   `json:"@id"` | 	RegistrationIndexURL string                   `json:"@id"` | ||||||
| 	Type                 []string                 `json:"@type"` | 	Type                 []string                 `json:"@type"` | ||||||
| @@ -53,7 +36,7 @@ type RegistrationIndexResponse struct { | |||||||
| 	Pages                []*RegistrationIndexPage `json:"items"` | 	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 { | type RegistrationIndexPage struct { | ||||||
| 	RegistrationPageURL string                       `json:"@id"` | 	RegistrationPageURL string                       `json:"@id"` | ||||||
| 	Lower               string                       `json:"lower"` | 	Lower               string                       `json:"lower"` | ||||||
| @@ -62,14 +45,14 @@ type RegistrationIndexPage struct { | |||||||
| 	Items               []*RegistrationIndexPageItem `json:"items"` | 	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 { | type RegistrationIndexPageItem struct { | ||||||
| 	RegistrationLeafURL string        `json:"@id"` | 	RegistrationLeafURL string        `json:"@id"` | ||||||
| 	PackageContentURL   string        `json:"packageContent"` | 	PackageContentURL   string        `json:"packageContent"` | ||||||
| 	CatalogEntry        *CatalogEntry `json:"catalogEntry"` | 	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 { | type CatalogEntry struct { | ||||||
| 	CatalogLeafURL           string                    `json:"@id"` | 	CatalogLeafURL           string                    `json:"@id"` | ||||||
| 	PackageContentURL        string                    `json:"packageContent"` | 	PackageContentURL        string                    `json:"packageContent"` | ||||||
| @@ -83,13 +66,13 @@ type CatalogEntry struct { | |||||||
| 	DependencyGroups         []*PackageDependencyGroup `json:"dependencyGroups"` | 	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 { | type PackageDependencyGroup struct { | ||||||
| 	TargetFramework string               `json:"targetFramework"` | 	TargetFramework string               `json:"targetFramework"` | ||||||
| 	Dependencies    []*PackageDependency `json:"dependencies"` | 	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 { | type PackageDependency struct { | ||||||
| 	ID    string `json:"id"` | 	ID    string `json:"id"` | ||||||
| 	Range string `json:"range"` | 	Range string `json:"range"` | ||||||
| @@ -162,7 +145,7 @@ func createDependencyGroups(pd *packages_model.PackageDescriptor) []*PackageDepe | |||||||
| 	return dependencyGroups | 	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 { | type RegistrationLeafResponse struct { | ||||||
| 	RegistrationLeafURL  string    `json:"@id"` | 	RegistrationLeafURL  string    `json:"@id"` | ||||||
| 	Type                 []string  `json:"@type"` | 	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 { | type PackageVersionsResponse struct { | ||||||
| 	Versions []string `json:"versions"` | 	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 { | type SearchResultResponse struct { | ||||||
| 	TotalHits int64           `json:"totalHits"` | 	TotalHits int64           `json:"totalHits"` | ||||||
| 	Data      []*SearchResult `json:"data"` | 	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 { | type SearchResult struct { | ||||||
| 	ID                   string                 `json:"id"` | 	ID                   string                 `json:"id"` | ||||||
| 	Version              string                 `json:"version"` | 	Version              string                 `json:"version"` | ||||||
| @@ -216,7 +199,7 @@ type SearchResult struct { | |||||||
| 	RegistrationIndexURL string                 `json:"registration"` | 	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 { | type SearchResultVersion struct { | ||||||
| 	RegistrationLeafURL string `json:"@id"` | 	RegistrationLeafURL string `json:"@id"` | ||||||
| 	Version             string `json:"version"` | 	Version             string `json:"version"` | ||||||
| @@ -26,3 +26,8 @@ func (l *linkBuilder) GetRegistrationLeafURL(id, version string) string { | |||||||
| func (l *linkBuilder) GetPackageDownloadURL(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) | 	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) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -5,15 +5,18 @@ | |||||||
| package nuget | package nuget | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"encoding/xml" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"regexp" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	packages_model "code.gitea.io/gitea/models/packages" | 	packages_model "code.gitea.io/gitea/models/packages" | ||||||
| 	"code.gitea.io/gitea/modules/context" | 	"code.gitea.io/gitea/modules/context" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	packages_module "code.gitea.io/gitea/modules/packages" | 	packages_module "code.gitea.io/gitea/modules/packages" | ||||||
| 	nuget_module "code.gitea.io/gitea/modules/packages/nuget" | 	nuget_module "code.gitea.io/gitea/modules/packages/nuget" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"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 xmlResponse(ctx *context.Context, status int, obj interface{}) { | ||||||
| func ServiceIndex(ctx *context.Context) { | 	ctx.Resp.Header().Set("Content-Type", "application/atom+xml; charset=utf-8") | ||||||
| 	resp := createServiceIndexResponse(setting.AppURL + "api/packages/" + ctx.Package.Owner.Name + "/nuget") | 	ctx.Resp.WriteHeader(status) | ||||||
|  | 	if _, err := ctx.Resp.Write([]byte(xml.Header)); err != nil { | ||||||
| 	ctx.JSON(http.StatusOK, resp) | 		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 | // https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs | ||||||
| func SearchService(ctx *context.Context) { | 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{ | 	pvs, count, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{ | ||||||
| 		OwnerID:    ctx.Package.Owner.ID, | 		OwnerID:    ctx.Package.Owner.ID, | ||||||
| 		Type:       packages_model.TypeNuGet, | 		Type:       packages_model.TypeNuGet, | ||||||
| @@ -69,7 +178,7 @@ func SearchService(ctx *context.Context) { | |||||||
| 	ctx.JSON(http.StatusOK, resp) | 	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) { | func RegistrationIndex(ctx *context.Context) { | ||||||
| 	packageName := ctx.Params("id") | 	packageName := ctx.Params("id") | ||||||
|  |  | ||||||
| @@ -97,8 +206,37 @@ func RegistrationIndex(ctx *context.Context) { | |||||||
| 	ctx.JSON(http.StatusOK, resp) | 	ctx.JSON(http.StatusOK, resp) | ||||||
| } | } | ||||||
|  |  | ||||||
| // RegistrationLeaf https://docs.microsoft.com/en-us/nuget/api/registration-base-url-resource#registration-leaf | // https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs | ||||||
| func RegistrationLeaf(ctx *context.Context) { | 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") | 	packageName := ctx.Params("id") | ||||||
| 	packageVersion := strings.TrimSuffix(ctx.Params("version"), ".json") | 	packageVersion := strings.TrimSuffix(ctx.Params("version"), ".json") | ||||||
|  |  | ||||||
| @@ -126,8 +264,33 @@ func RegistrationLeaf(ctx *context.Context) { | |||||||
| 	ctx.JSON(http.StatusOK, resp) | 	ctx.JSON(http.StatusOK, resp) | ||||||
| } | } | ||||||
|  |  | ||||||
| // EnumeratePackageVersions https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions | // https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs | ||||||
| func EnumeratePackageVersions(ctx *context.Context) { | 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") | 	packageName := ctx.Params("id") | ||||||
|  |  | ||||||
| 	pvs, err := packages_model.GetVersionsByPackageName(ctx, ctx.Package.Owner.ID, packages_model.TypeNuGet, packageName) | 	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) | 	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) { | func DownloadPackageFile(ctx *context.Context) { | ||||||
| 	packageName := ctx.Params("id") | 	packageName := ctx.Params("id") | ||||||
| 	packageVersion := ctx.Params("version") | 	packageVersion := ctx.Params("version") | ||||||
| @@ -350,7 +513,7 @@ func processUploadedFile(ctx *context.Context, expectedType nuget_module.Package | |||||||
| 	return np, buf, closables | 	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) { | func DownloadSymbolFile(ctx *context.Context) { | ||||||
| 	filename := ctx.Params("filename") | 	filename := ctx.Params("filename") | ||||||
| 	guid := ctx.Params("guid")[:32] | 	guid := ctx.Params("guid")[:32] | ||||||
|   | |||||||
| @@ -8,10 +8,13 @@ import ( | |||||||
| 	"archive/zip" | 	"archive/zip" | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
|  | 	"encoding/xml" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"net/http/httptest" | ||||||
| 	"testing" | 	"testing" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models/db" | 	"code.gitea.io/gitea/models/db" | ||||||
| 	"code.gitea.io/gitea/models/packages" | 	"code.gitea.io/gitea/models/packages" | ||||||
| @@ -31,9 +34,45 @@ func addNuGetAPIKeyHeader(request *http.Request, token string) *http.Request { | |||||||
| 	return 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) { | func TestPackageNuGet(t *testing.T) { | ||||||
| 	defer tests.PrepareTestEnv(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}) | 	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) | ||||||
| 	token := getUserToken(t, user.Name) | 	token := getUserToken(t, user.Name) | ||||||
|  |  | ||||||
| @@ -54,9 +93,11 @@ func TestPackageNuGet(t *testing.T) { | |||||||
| 		<version>` + packageVersion + `</version> | 		<version>` + packageVersion + `</version> | ||||||
| 		<authors>` + packageAuthors + `</authors> | 		<authors>` + packageAuthors + `</authors> | ||||||
| 		<description>` + packageDescription + `</description> | 		<description>` + packageDescription + `</description> | ||||||
| 		<group targetFramework=".NETStandard2.0"> | 		<dependencies> | ||||||
| 			<dependency id="Microsoft.CSharp" version="4.5.0" /> | 			<group targetFramework=".NETStandard2.0"> | ||||||
| 		</group> | 				<dependency id="Microsoft.CSharp" version="4.5.0" /> | ||||||
|  | 			</group> | ||||||
|  | 		</dependencies> | ||||||
| 	  </metadata> | 	  </metadata> | ||||||
| 	</package>`)) | 	</package>`)) | ||||||
| 	archive.Close() | 	archive.Close() | ||||||
| @@ -67,60 +108,101 @@ func TestPackageNuGet(t *testing.T) { | |||||||
| 	t.Run("ServiceIndex", func(t *testing.T) { | 	t.Run("ServiceIndex", func(t *testing.T) { | ||||||
| 		defer tests.PrintCurrentTest(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 { | 			privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate}) | ||||||
| 			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 { | 			cases := []struct { | ||||||
| 			url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner) | 				Owner        string | ||||||
|  | 				UseBasicAuth bool | ||||||
| 			req := NewRequest(t, "GET", fmt.Sprintf("%s/index.json", url)) | 				UseTokenAuth bool | ||||||
| 			if c.UseBasicAuth { | 			}{ | ||||||
| 				req = AddBasicAuthHeader(req, user.Name) | 				{privateUser.Name, false, false}, | ||||||
| 			} else if c.UseTokenAuth { | 				{privateUser.Name, true, false}, | ||||||
| 				req = addNuGetAPIKeyHeader(req, token) | 				{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 | 			for _, c := range cases { | ||||||
| 			DecodeJSON(t, resp, &result) | 				url := fmt.Sprintf("/api/packages/%s/nuget", c.Owner) | ||||||
|  |  | ||||||
| 			assert.Equal(t, "3.0.0", result.Version) | 				req := NewRequest(t, "GET", url) | ||||||
| 			assert.NotEmpty(t, result.Resources) | 				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:] | 				var result nuget.ServiceIndexResponseV2 | ||||||
| 			for _, r := range result.Resources { | 				decodeXML(t, resp, &result) | ||||||
| 				switch r.Type { |  | ||||||
| 				case "SearchQueryService": | 				assert.Equal(t, setting.AppURL+url[1:], result.Base) | ||||||
| 					fallthrough | 				assert.Equal(t, "Packages", result.Workspace.Collection.Href) | ||||||
| 				case "SearchQueryService/3.0.0-beta": | 			} | ||||||
| 					fallthrough | 		}) | ||||||
| 				case "SearchQueryService/3.0.0-rc": |  | ||||||
| 					assert.Equal(t, root+"/query", r.ID) | 		t.Run("v3", func(t *testing.T) { | ||||||
| 				case "RegistrationsBaseUrl": | 			defer tests.PrintCurrentTest(t)() | ||||||
| 					fallthrough |  | ||||||
| 				case "RegistrationsBaseUrl/3.0.0-beta": | 			privateUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{Visibility: structs.VisibleTypePrivate}) | ||||||
| 					fallthrough |  | ||||||
| 				case "RegistrationsBaseUrl/3.0.0-rc": | 			cases := []struct { | ||||||
| 					assert.Equal(t, root+"/registration", r.ID) | 				Owner        string | ||||||
| 				case "PackageBaseAddress/3.0.0": | 				UseBasicAuth bool | ||||||
| 					assert.Equal(t, root+"/package", r.ID) | 				UseTokenAuth bool | ||||||
| 				case "PackagePublish/2.0.0": | 			}{ | ||||||
| 					assert.Equal(t, root, r.ID) | 				{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) { | 	t.Run("Upload", func(t *testing.T) { | ||||||
| @@ -305,17 +387,57 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) | |||||||
| 			{"test", 1, 10, 1, 0}, | 			{"test", 1, 10, 1, 0}, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		for i, c := range cases { | 		t.Run("v2", func(t *testing.T) { | ||||||
| 			req := NewRequest(t, "GET", fmt.Sprintf("%s/query?q=%s&skip=%d&take=%d", url, c.Query, c.Skip, c.Take)) | 			defer tests.PrintCurrentTest(t)() | ||||||
| 			req = AddBasicAuthHeader(req, user.Name) |  | ||||||
| 			resp := MakeRequest(t, req, http.StatusOK) |  | ||||||
|  |  | ||||||
| 			var result nuget.SearchResultResponse | 			t.Run("Search()", func(t *testing.T) { | ||||||
| 			DecodeJSON(t, resp, &result) | 				defer tests.PrintCurrentTest(t)() | ||||||
|  |  | ||||||
| 			assert.Equal(t, c.ExpectedTotal, result.TotalHits, "case %d: unexpected total hits", i) | 				for i, c := range cases { | ||||||
| 			assert.Len(t, result.Data, c.ExpectedResults, "case %d: unexpected result count", i) | 					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) { | 	t.Run("RegistrationService", func(t *testing.T) { | ||||||
| @@ -352,31 +474,70 @@ AAAjQmxvYgAAAGm7ENm9SGxMtAFVvPUsPJTF6PbtAAAAAFcVogEJAAAAAQAAAA==`) | |||||||
| 		t.Run("RegistrationLeaf", func(t *testing.T) { | 		t.Run("RegistrationLeaf", func(t *testing.T) { | ||||||
| 			defer tests.PrintCurrentTest(t)() | 			defer tests.PrintCurrentTest(t)() | ||||||
|  |  | ||||||
| 			req := NewRequest(t, "GET", fmt.Sprintf("%s/registration/%s/%s.json", url, packageName, packageVersion)) | 			t.Run("v2", func(t *testing.T) { | ||||||
| 			req = AddBasicAuthHeader(req, user.Name) | 				defer tests.PrintCurrentTest(t)() | ||||||
| 			resp := MakeRequest(t, req, http.StatusOK) |  | ||||||
|  |  | ||||||
| 			var result nuget.RegistrationLeafResponse | 				req := NewRequest(t, "GET", fmt.Sprintf("%s/Packages(Id='%s',Version='%s')", url, packageName, packageVersion)) | ||||||
| 			DecodeJSON(t, resp, &result) | 				req = AddBasicAuthHeader(req, user.Name) | ||||||
|  | 				resp := MakeRequest(t, req, http.StatusOK) | ||||||
|  |  | ||||||
| 			assert.Equal(t, leafURL, result.RegistrationLeafURL) | 				var result FeedEntry | ||||||
| 			assert.Equal(t, contentURL, result.PackageContentURL) | 				decodeXML(t, resp, &result) | ||||||
| 			assert.Equal(t, indexURL, result.RegistrationIndexURL) |  | ||||||
|  | 				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) { | 	t.Run("PackageService", func(t *testing.T) { | ||||||
| 		defer tests.PrintCurrentTest(t)() | 		defer tests.PrintCurrentTest(t)() | ||||||
|  |  | ||||||
| 		req := NewRequest(t, "GET", fmt.Sprintf("%s/package/%s/index.json", url, packageName)) | 		t.Run("v2", func(t *testing.T) { | ||||||
| 		req = AddBasicAuthHeader(req, user.Name) | 			defer tests.PrintCurrentTest(t)() | ||||||
| 		resp := MakeRequest(t, req, http.StatusOK) |  | ||||||
|  |  | ||||||
| 		var result nuget.PackageVersionsResponse | 			req := NewRequest(t, "GET", fmt.Sprintf("%s/FindPackagesById()?id='%s'", url, packageName)) | ||||||
| 		DecodeJSON(t, resp, &result) | 			req = AddBasicAuthHeader(req, user.Name) | ||||||
|  | 			resp := MakeRequest(t, req, http.StatusOK) | ||||||
|  |  | ||||||
| 		assert.Len(t, result.Versions, 1) | 			var result FeedResponse | ||||||
| 		assert.Equal(t, packageVersion, result.Versions[0]) | 			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) { | 	t.Run("Delete", func(t *testing.T) { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user