Add Package Registry (#16510)

* Added package store settings.

* Added models.

* Added generic package registry.

* Added tests.

* Added NuGet package registry.

* Moved service index to api file.

* Added NPM package registry.

* Added Maven package registry.

* Added PyPI package registry.

* Summary is deprecated.

* Changed npm name.

* Sanitize project url.

* Allow only scoped packages.

* Added user interface.

* Changed method name.

* Added missing migration file.

* Set page info.

* Added documentation.

* Added documentation links.

* Fixed wrong error message.

* Lint template files.

* Fixed merge errors.

* Fixed unit test storage path.

* Switch to json module.

* Added suggestions.

* Added package webhook.

* Add package api.

* Fixed swagger file.

* Fixed enum and comments.

* Fixed NuGet pagination.

* Print test names.

* Added api tests.

* Fixed access level.

* Fix User unmarshal.

* Added RubyGems package registry.

* Fix lint.

* Implemented io.Writer.

* Added support for sha256/sha512 checksum files.

* Improved maven-metadata.xml support.

* Added support for symbol package uploads.

* Added tests.

* Added overview docs.

* Added npm dependencies and keywords.

* Added no-packages information.

* Display file size.

* Display asset count.

* Fixed filter alignment.

* Added package icons.

* Formatted instructions.

* Allow anonymous package downloads.

* Fixed comments.

* Fixed postgres test.

* Moved file.

* Moved models to models/packages.

* Use correct error response format per client.

* Use simpler search form.

* Fixed IsProd.

* Restructured data model.

* Prevent empty filename.

* Fix swagger.

* Implemented user/org registry.

* Implemented UI.

* Use GetUserByIDCtx.

* Use table for dependencies.

* make svg

* Added support for unscoped npm packages.

* Add support for npm dist tags.

* Added tests for npm tags.

* Unlink packages if repository gets deleted.

* Prevent user/org delete if a packages exist.

* Use package unlink in repository service.

* Added support for composer packages.

* Restructured package docs.

* Added missing tests.

* Fixed generic content page.

* Fixed docs.

* Fixed swagger.

* Added missing type.

* Fixed ambiguous column.

* Organize content store by sha256 hash.

* Added admin package management.

* Added support for sorting.

* Add support for multiple identical versions/files.

* Added missing repository unlink.

* Added file properties.

* make fmt

* lint

* Added Conan package registry.

* Updated docs.

* Unify package names.

* Added swagger enum.

* Use longer TEXT column type.

* Removed version composite key.

* Merged package and container registry.

* Removed index.

* Use dedicated package router.

* Moved files to new location.

* Updated docs.

* Fixed JOIN order.

* Fixed GROUP BY statement.

* Fixed GROUP BY #2.

* Added symbol server support.

* Added more tests.

* Set NOT NULL.

* Added setting to disable package registries.

* Moved auth into service.

* refactor

* Use ctx everywhere.

* Added package cleanup task.

* Changed packages path.

* Added container registry.

* Refactoring

* Updated comparison.

* Fix swagger.

* Fixed table order.

* Use token auth for npm routes.

* Enabled ReverseProxy auth.

* Added packages link for orgs.

* Fixed anonymous org access.

* Enable copy button for setup instructions.

* Merge error

* Added suggestions.

* Fixed merge.

* Handle "generic".

* Added link for TODO.

* Added suggestions.

* Changed temporary buffer filename.

* Added suggestions.

* Apply suggestions from code review

Co-authored-by: Thomas Boerger <thomas@webhippie.de>

* Update docs/content/doc/packages/nuget.en-us.md

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Thomas Boerger <thomas@webhippie.de>
This commit is contained in:
KN4CK3R 2022-03-30 10:42:47 +02:00 committed by GitHub
parent 2bce1ea986
commit 1d332342db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
197 changed files with 18563 additions and 55 deletions

View file

@ -0,0 +1,311 @@
// Copyright 2021 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 rubygems
import (
"bufio"
"bytes"
"errors"
"io"
"reflect"
)
const (
majorVersion = 4
minorVersion = 8
typeNil = '0'
typeTrue = 'T'
typeFalse = 'F'
typeFixnum = 'i'
typeString = '"'
typeSymbol = ':'
typeSymbolLink = ';'
typeArray = '['
typeIVar = 'I'
typeUserMarshal = 'U'
typeUserDef = 'u'
typeObject = 'o'
)
var (
// ErrUnsupportedType indicates an unsupported type
ErrUnsupportedType = errors.New("Type is unsupported")
// ErrInvalidIntRange indicates an invalid number range
ErrInvalidIntRange = errors.New("Number is not in valid range")
)
// RubyUserMarshal is a Ruby object that has a marshal_load function.
type RubyUserMarshal struct {
Name string
Value interface{}
}
// RubyUserDef is a Ruby object that has a _load function.
type RubyUserDef struct {
Name string
Value interface{}
}
// RubyObject is a default Ruby object.
type RubyObject struct {
Name string
Member map[string]interface{}
}
// MarshalEncoder mimics Rubys Marshal class.
// Note: Only supports types used by the RubyGems package registry.
type MarshalEncoder struct {
w *bufio.Writer
symbols map[string]int
}
// NewMarshalEncoder creates a new MarshalEncoder
func NewMarshalEncoder(w io.Writer) *MarshalEncoder {
return &MarshalEncoder{
w: bufio.NewWriter(w),
symbols: map[string]int{},
}
}
// Encode encodes the given type
func (e *MarshalEncoder) Encode(v interface{}) error {
if _, err := e.w.Write([]byte{majorVersion, minorVersion}); err != nil {
return err
}
if err := e.marshal(v); err != nil {
return err
}
return e.w.Flush()
}
func (e *MarshalEncoder) marshal(v interface{}) error {
if v == nil {
return e.marshalNil()
}
val := reflect.ValueOf(v)
typ := reflect.TypeOf(v)
if typ.Kind() == reflect.Ptr {
val = val.Elem()
typ = typ.Elem()
}
switch typ.Kind() {
case reflect.Bool:
return e.marshalBool(val.Bool())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
return e.marshalInt(val.Int())
case reflect.String:
return e.marshalString(val.String())
case reflect.Slice, reflect.Array:
return e.marshalArray(val)
}
switch typ.Name() {
case "RubyUserMarshal":
return e.marshalUserMarshal(val.Interface().(RubyUserMarshal))
case "RubyUserDef":
return e.marshalUserDef(val.Interface().(RubyUserDef))
case "RubyObject":
return e.marshalObject(val.Interface().(RubyObject))
}
return ErrUnsupportedType
}
func (e *MarshalEncoder) marshalNil() error {
return e.w.WriteByte(typeNil)
}
func (e *MarshalEncoder) marshalBool(b bool) error {
if b {
return e.w.WriteByte(typeTrue)
}
return e.w.WriteByte(typeFalse)
}
func (e *MarshalEncoder) marshalInt(i int64) error {
if err := e.w.WriteByte(typeFixnum); err != nil {
return err
}
return e.marshalIntInternal(i)
}
func (e *MarshalEncoder) marshalIntInternal(i int64) error {
if i == 0 {
return e.w.WriteByte(0)
} else if 0 < i && i < 123 {
return e.w.WriteByte(byte(i + 5))
} else if -124 < i && i <= -1 {
return e.w.WriteByte(byte(i - 5))
}
var len int
if 122 < i && i <= 0xff {
len = 1
} else if 0xff < i && i <= 0xffff {
len = 2
} else if 0xffff < i && i <= 0xffffff {
len = 3
} else if 0xffffff < i && i <= 0x3fffffff {
len = 4
} else if -0x100 <= i && i < -123 {
len = -1
} else if -0x10000 <= i && i < -0x100 {
len = -2
} else if -0x1000000 <= i && i < -0x100000 {
len = -3
} else if -0x40000000 <= i && i < -0x1000000 {
len = -4
} else {
return ErrInvalidIntRange
}
if err := e.w.WriteByte(byte(len)); err != nil {
return err
}
if len < 0 {
len = -len
}
for c := 0; c < len; c++ {
if err := e.w.WriteByte(byte(i >> uint(8*c) & 0xff)); err != nil {
return err
}
}
return nil
}
func (e *MarshalEncoder) marshalString(str string) error {
if err := e.w.WriteByte(typeIVar); err != nil {
return err
}
if err := e.marshalRawString(str); err != nil {
return err
}
if err := e.marshalIntInternal(1); err != nil {
return err
}
if err := e.marshalSymbol("E"); err != nil {
return err
}
return e.marshalBool(true)
}
func (e *MarshalEncoder) marshalRawString(str string) error {
if err := e.w.WriteByte(typeString); err != nil {
return err
}
if err := e.marshalIntInternal(int64(len(str))); err != nil {
return err
}
_, err := e.w.WriteString(str)
return err
}
func (e *MarshalEncoder) marshalSymbol(str string) error {
if index, ok := e.symbols[str]; ok {
if err := e.w.WriteByte(typeSymbolLink); err != nil {
return err
}
return e.marshalIntInternal(int64(index))
}
e.symbols[str] = len(e.symbols)
if err := e.w.WriteByte(typeSymbol); err != nil {
return err
}
if err := e.marshalIntInternal(int64(len(str))); err != nil {
return err
}
_, err := e.w.WriteString(str)
return err
}
func (e *MarshalEncoder) marshalArray(arr reflect.Value) error {
if err := e.w.WriteByte(typeArray); err != nil {
return err
}
len := arr.Len()
if err := e.marshalIntInternal(int64(len)); err != nil {
return err
}
for i := 0; i < len; i++ {
if err := e.marshal(arr.Index(i).Interface()); err != nil {
return err
}
}
return nil
}
func (e *MarshalEncoder) marshalUserMarshal(userMarshal RubyUserMarshal) error {
if err := e.w.WriteByte(typeUserMarshal); err != nil {
return err
}
if err := e.marshalSymbol(userMarshal.Name); err != nil {
return err
}
return e.marshal(userMarshal.Value)
}
func (e *MarshalEncoder) marshalUserDef(userDef RubyUserDef) error {
var buf bytes.Buffer
if err := NewMarshalEncoder(&buf).Encode(userDef.Value); err != nil {
return err
}
if err := e.w.WriteByte(typeUserDef); err != nil {
return err
}
if err := e.marshalSymbol(userDef.Name); err != nil {
return err
}
if err := e.marshalIntInternal(int64(buf.Len())); err != nil {
return err
}
_, err := e.w.Write(buf.Bytes())
return err
}
func (e *MarshalEncoder) marshalObject(obj RubyObject) error {
if err := e.w.WriteByte(typeObject); err != nil {
return err
}
if err := e.marshalSymbol(obj.Name); err != nil {
return err
}
if err := e.marshalIntInternal(int64(len(obj.Member))); err != nil {
return err
}
for k, v := range obj.Member {
if err := e.marshalSymbol(k); err != nil {
return err
}
if err := e.marshal(v); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,99 @@
// Copyright 2021 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 rubygems
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMinimalEncoder(t *testing.T) {
cases := []struct {
Value interface{}
Expected []byte
Error error
}{
{
Value: nil,
Expected: []byte{4, 8, 0x30},
},
{
Value: true,
Expected: []byte{4, 8, 'T'},
},
{
Value: false,
Expected: []byte{4, 8, 'F'},
},
{
Value: 0,
Expected: []byte{4, 8, 'i', 0},
},
{
Value: 1,
Expected: []byte{4, 8, 'i', 6},
},
{
Value: -1,
Expected: []byte{4, 8, 'i', 0xfa},
},
{
Value: 0x1fffffff,
Expected: []byte{4, 8, 'i', 4, 0xff, 0xff, 0xff, 0x1f},
},
{
Value: 0x41000000,
Error: ErrInvalidIntRange,
},
{
Value: "test",
Expected: []byte{4, 8, 'I', '"', 9, 't', 'e', 's', 't', 6, ':', 6, 'E', 'T'},
},
{
Value: []int{1, 2},
Expected: []byte{4, 8, '[', 7, 'i', 6, 'i', 7},
},
{
Value: &RubyUserMarshal{
Name: "Test",
Value: 4,
},
Expected: []byte{4, 8, 'U', ':', 9, 'T', 'e', 's', 't', 'i', 9},
},
{
Value: &RubyUserDef{
Name: "Test",
Value: 4,
},
Expected: []byte{4, 8, 'u', ':', 9, 'T', 'e', 's', 't', 9, 4, 8, 'i', 9},
},
{
Value: &RubyObject{
Name: "Test",
Member: map[string]interface{}{
"test": 4,
},
},
Expected: []byte{4, 8, 'o', ':', 9, 'T', 'e', 's', 't', 6, ':', 9, 't', 'e', 's', 't', 'i', 9},
},
{
Value: &struct {
Name string
}{
"test",
},
Error: ErrUnsupportedType,
},
}
for i, c := range cases {
var b bytes.Buffer
err := NewMarshalEncoder(&b).Encode(c.Value)
assert.ErrorIs(t, err, c.Error)
assert.Equal(t, c.Expected, b.Bytes(), "case %d", i)
}
}

View file

@ -0,0 +1,222 @@
// Copyright 2021 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 rubygems
import (
"archive/tar"
"compress/gzip"
"errors"
"io"
"regexp"
"strings"
"code.gitea.io/gitea/modules/validation"
"gopkg.in/yaml.v2"
)
var (
// ErrMissingMetadataFile indicates a missing metadata.gz file
ErrMissingMetadataFile = errors.New("Metadata file is missing")
// ErrInvalidName indicates an invalid id in the metadata.gz file
ErrInvalidName = errors.New("Metadata file contains an invalid name")
// ErrInvalidVersion indicates an invalid version in the metadata.gz file
ErrInvalidVersion = errors.New("Metadata file contains an invalid version")
)
var versionMatcher = regexp.MustCompile(`\A[0-9]+(?:\.[0-9a-zA-Z]+)*(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?\z`)
// Package represents a RubyGems package
type Package struct {
Name string
Version string
Metadata *Metadata
}
// Metadata represents the metadata of a RubyGems package
type Metadata struct {
Platform string `json:"platform,omitempty"`
Description string `json:"description,omitempty"`
Summary string `json:"summary,omitempty"`
Authors []string `json:"authors,omitempty"`
Licenses []string `json:"licenses,omitempty"`
RequiredRubyVersion []VersionRequirement `json:"required_ruby_version,omitempty"`
RequiredRubygemsVersion []VersionRequirement `json:"required_rubygems_version,omitempty"`
ProjectURL string `json:"project_url,omitempty"`
RuntimeDependencies []Dependency `json:"runtime_dependencies,omitempty"`
DevelopmentDependencies []Dependency `json:"development_dependencies,omitempty"`
}
// VersionRequirement represents a version restriction
type VersionRequirement struct {
Restriction string `json:"restriction"`
Version string `json:"version"`
}
// Dependency represents a dependency of a RubyGems package
type Dependency struct {
Name string `json:"name"`
Version []VersionRequirement `json:"version"`
}
type gemspec struct {
Name string `yaml:"name"`
Version struct {
Version string `yaml:"version"`
} `yaml:"version"`
Platform string `yaml:"platform"`
Authors []string `yaml:"authors"`
Autorequire interface{} `yaml:"autorequire"`
Bindir string `yaml:"bindir"`
CertChain []interface{} `yaml:"cert_chain"`
Date string `yaml:"date"`
Dependencies []struct {
Name string `yaml:"name"`
Requirement requirement `yaml:"requirement"`
Type string `yaml:"type"`
Prerelease bool `yaml:"prerelease"`
VersionRequirements requirement `yaml:"version_requirements"`
} `yaml:"dependencies"`
Description string `yaml:"description"`
Email string `yaml:"email"`
Executables []string `yaml:"executables"`
Extensions []interface{} `yaml:"extensions"`
ExtraRdocFiles []string `yaml:"extra_rdoc_files"`
Files []string `yaml:"files"`
Homepage string `yaml:"homepage"`
Licenses []string `yaml:"licenses"`
Metadata struct {
BugTrackerURI string `yaml:"bug_tracker_uri"`
ChangelogURI string `yaml:"changelog_uri"`
DocumentationURI string `yaml:"documentation_uri"`
SourceCodeURI string `yaml:"source_code_uri"`
} `yaml:"metadata"`
PostInstallMessage interface{} `yaml:"post_install_message"`
RdocOptions []interface{} `yaml:"rdoc_options"`
RequirePaths []string `yaml:"require_paths"`
RequiredRubyVersion requirement `yaml:"required_ruby_version"`
RequiredRubygemsVersion requirement `yaml:"required_rubygems_version"`
Requirements []interface{} `yaml:"requirements"`
RubygemsVersion string `yaml:"rubygems_version"`
SigningKey interface{} `yaml:"signing_key"`
SpecificationVersion int `yaml:"specification_version"`
Summary string `yaml:"summary"`
TestFiles []interface{} `yaml:"test_files"`
}
type requirement struct {
Requirements [][]interface{} `yaml:"requirements"`
}
// AsVersionRequirement converts into []VersionRequirement
func (r requirement) AsVersionRequirement() []VersionRequirement {
requirements := make([]VersionRequirement, 0, len(r.Requirements))
for _, req := range r.Requirements {
if len(req) != 2 {
continue
}
restriction, ok := req[0].(string)
if !ok {
continue
}
vm, ok := req[1].(map[interface{}]interface{})
if !ok {
continue
}
versionInt, ok := vm["version"]
if !ok {
continue
}
version, ok := versionInt.(string)
if !ok || version == "0" {
continue
}
requirements = append(requirements, VersionRequirement{
Restriction: restriction,
Version: version,
})
}
return requirements
}
// ParsePackageMetaData parses the metadata of a Gem package file
func ParsePackageMetaData(r io.Reader) (*Package, error) {
archive := tar.NewReader(r)
for {
hdr, err := archive.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hdr.Name == "metadata.gz" {
return parseMetadataFile(archive)
}
}
return nil, ErrMissingMetadataFile
}
func parseMetadataFile(r io.Reader) (*Package, error) {
zr, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer zr.Close()
var spec gemspec
if err := yaml.NewDecoder(zr).Decode(&spec); err != nil {
return nil, err
}
if len(spec.Name) == 0 || strings.Contains(spec.Name, "/") {
return nil, ErrInvalidName
}
if !versionMatcher.MatchString(spec.Version.Version) {
return nil, ErrInvalidVersion
}
if !validation.IsValidURL(spec.Homepage) {
spec.Homepage = ""
}
if !validation.IsValidURL(spec.Metadata.SourceCodeURI) {
spec.Metadata.SourceCodeURI = ""
}
m := &Metadata{
Platform: spec.Platform,
Description: spec.Description,
Summary: spec.Summary,
Authors: spec.Authors,
Licenses: spec.Licenses,
ProjectURL: spec.Homepage,
RequiredRubyVersion: spec.RequiredRubyVersion.AsVersionRequirement(),
RequiredRubygemsVersion: spec.RequiredRubygemsVersion.AsVersionRequirement(),
DevelopmentDependencies: make([]Dependency, 0, 5),
RuntimeDependencies: make([]Dependency, 0, 5),
}
for _, gemdep := range spec.Dependencies {
dep := Dependency{
Name: gemdep.Name,
Version: gemdep.Requirement.AsVersionRequirement(),
}
if gemdep.Type == ":runtime" {
m.RuntimeDependencies = append(m.RuntimeDependencies, dep)
} else {
m.DevelopmentDependencies = append(m.DevelopmentDependencies, dep)
}
}
return &Package{
Name: spec.Name,
Version: spec.Version.Version,
Metadata: m,
}, nil
}

View file

@ -0,0 +1,89 @@
// Copyright 2021 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 rubygems
import (
"archive/tar"
"bytes"
"encoding/base64"
"io"
"testing"
"github.com/stretchr/testify/assert"
)
func TestParsePackageMetaData(t *testing.T) {
createArchive := func(filename string, content []byte) io.Reader {
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
hdr := &tar.Header{
Name: filename,
Mode: 0o600,
Size: int64(len(content)),
}
tw.WriteHeader(hdr)
tw.Write(content)
tw.Close()
return &buf
}
t.Run("MissingMetadataFile", func(t *testing.T) {
data := createArchive("dummy.txt", []byte{0})
rp, err := ParsePackageMetaData(data)
assert.ErrorIs(t, err, ErrMissingMetadataFile)
assert.Nil(t, rp)
})
t.Run("Valid", func(t *testing.T) {
content, _ := base64.StdEncoding.DecodeString("H4sICHC/I2EEAG1ldGFkYXRhAAEeAOH/bmFtZTogZwp2ZXJzaW9uOgogIHZlcnNpb246IDEKWw35Tx4AAAA=")
data := createArchive("metadata.gz", content)
rp, err := ParsePackageMetaData(data)
assert.NoError(t, err)
assert.NotNil(t, rp)
})
}
func TestParseMetadataFile(t *testing.T) {
content, _ := base64.StdEncoding.DecodeString(`H4sIAMe7I2ECA9VVTW/UMBC9+1eYXvaUbJpSQBZUHJAqDlwK4kCFIseZzZrGH9iTqisEv52Js9nd
0KqggiqRXWnX45n3ZuZ5nCzL+JPQ15ulq7+AQnEORoj3HpReaSVRO8usNCB4qxEku4YQySbuCPo4
bjHOd07HeZGfMt9JXLlgBB9imOxx7UIULOPnCZMMLsDXXgeiYbW2jQ6C0y9TELBSa6kJ6/IzaySS
R1mUx1nxIitPeFGI9M2L6eGfWAMebANWaUgktzN9M3lsKNmxutBb1AYyCibbNhsDFu+q9GK/Tc4z
d2IcLBl9js5eHaXFsLyvXeNz0LQyL/YoLx8EsiCMBZlx46k6sS2PDD5AgA5kJPNKdhH2elWzOv7n
uv9Q9Aau/6ngP84elvNpXh5oRVlB5/yW7BH0+qu0G4gqaI/JdEHBFBS5l+pKtsARIjIwUnfj8Le0
+TrdJLl2DG5A9SjrjgZ1mG+4QbAD+G4ZZBUap6qVnnzGf6Rwp+vliBRqtnYGPBEKvkb0USyXE8mS
dVoR6hj07u0HZgAl3SRS8G/fmXcRK20jyq6rDMSYQFgidamqkXbbuspLXE/0k7GphtKqe67GuRC/
yjAbmt9LsOMp8xMamFkSQ38fP5EFjdz8LA4do2C69VvqWXAJgrPbKZb58/xZXrKoW6ttW13Bhvzi
4ftn7/yUxd4YGcglvTmmY8aGY3ZwRn4CqcWcidUGAAA=`)
rp, err := parseMetadataFile(bytes.NewReader(content))
assert.NoError(t, err)
assert.NotNil(t, rp)
assert.Equal(t, "gitea", rp.Name)
assert.Equal(t, "1.0.5", rp.Version)
assert.Equal(t, "ruby", rp.Metadata.Platform)
assert.Equal(t, "Gitea package", rp.Metadata.Summary)
assert.Equal(t, "RubyGems package test", rp.Metadata.Description)
assert.Equal(t, []string{"Gitea"}, rp.Metadata.Authors)
assert.Equal(t, "https://gitea.io/", rp.Metadata.ProjectURL)
assert.Equal(t, []string{"MIT"}, rp.Metadata.Licenses)
assert.Empty(t, rp.Metadata.RequiredRubygemsVersion)
assert.Len(t, rp.Metadata.RequiredRubyVersion, 1)
assert.Equal(t, ">=", rp.Metadata.RequiredRubyVersion[0].Restriction)
assert.Equal(t, "2.3.0", rp.Metadata.RequiredRubyVersion[0].Version)
assert.Len(t, rp.Metadata.RuntimeDependencies, 1)
assert.Equal(t, "runtime-dep", rp.Metadata.RuntimeDependencies[0].Name)
assert.Len(t, rp.Metadata.RuntimeDependencies[0].Version, 2)
assert.Equal(t, ">=", rp.Metadata.RuntimeDependencies[0].Version[0].Restriction)
assert.Equal(t, "1.2.0", rp.Metadata.RuntimeDependencies[0].Version[0].Version)
assert.Equal(t, "<", rp.Metadata.RuntimeDependencies[0].Version[1].Restriction)
assert.Equal(t, "2.0", rp.Metadata.RuntimeDependencies[0].Version[1].Version)
assert.Len(t, rp.Metadata.DevelopmentDependencies, 1)
assert.Equal(t, "dev-dep", rp.Metadata.DevelopmentDependencies[0].Name)
assert.Len(t, rp.Metadata.DevelopmentDependencies[0].Version, 1)
assert.Equal(t, "~>", rp.Metadata.DevelopmentDependencies[0].Version[0].Restriction)
assert.Equal(t, "5.2", rp.Metadata.DevelopmentDependencies[0].Version[0].Version)
}