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:
parent
c35531dd11
commit
0e58201d1a
8 changed files with 850 additions and 135 deletions
|
@ -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**
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
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"
|
||||
)
|
||||
|
||||
// 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"`
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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) {
|
|||
<version>` + packageVersion + `</version>
|
||||
<authors>` + packageAuthors + `</authors>
|
||||
<description>` + packageDescription + `</description>
|
||||
<group targetFramework=".NETStandard2.0">
|
||||
<dependency id="Microsoft.CSharp" version="4.5.0" />
|
||||
</group>
|
||||
<dependencies>
|
||||
<group targetFramework=".NETStandard2.0">
|
||||
<dependency id="Microsoft.CSharp" version="4.5.0" />
|
||||
</group>
|
||||
</dependencies>
|
||||
</metadata>
|
||||
</package>`))
|
||||
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) {
|
||||
|
|
Loading…
Reference in a new issue