mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
* initial pypi registry implementation * support proxy mode for pypi package managers * support proxy mode for pypi package managers - 2 * rm default mode as proxy for pip3 * update goproxy version & fix pypi proxy failing on 304 * add PIP_RETRIES=0 env * update pmg e2e & add proxy mode e2e for pypi * rm safedep-test-pkg for pypi proxy e2e
591 lines
14 KiB
Go
591 lines
14 KiB
Go
package interceptors
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestPypiFilesParser_ParseURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
urlPath string
|
|
wantName string
|
|
wantVersion string
|
|
wantIsDownload bool
|
|
wantFileType string
|
|
wantErr bool
|
|
}{
|
|
// Source distributions (sdist)
|
|
{
|
|
name: "sdist tar.gz simple package",
|
|
urlPath: "/packages/ab/cd/abcd1234/requests-2.28.0.tar.gz",
|
|
wantName: "requests",
|
|
wantVersion: "2.28.0",
|
|
wantIsDownload: true,
|
|
wantFileType: "sdist",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "sdist tar.gz with hyphenated name",
|
|
urlPath: "/packages/12/34/5678abcd/Flask-RESTful-0.3.10.tar.gz",
|
|
wantName: "flask-restful",
|
|
wantVersion: "0.3.10",
|
|
wantIsDownload: true,
|
|
wantFileType: "sdist",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "sdist zip format",
|
|
urlPath: "/packages/aa/bb/ccdd/some-package-1.0.0.zip",
|
|
wantName: "some-package",
|
|
wantVersion: "1.0.0",
|
|
wantIsDownload: true,
|
|
wantFileType: "sdist",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "sdist with prerelease version",
|
|
urlPath: "/packages/ff/ee/ddcc/mypackage-2.0.0rc1.tar.gz",
|
|
wantName: "mypackage",
|
|
wantVersion: "2.0.0rc1",
|
|
wantIsDownload: true,
|
|
wantFileType: "sdist",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "sdist with dev version",
|
|
urlPath: "/packages/11/22/3344/testpkg-0.1.0.dev1.tar.gz",
|
|
wantName: "testpkg",
|
|
wantVersion: "0.1.0.dev1",
|
|
wantIsDownload: true,
|
|
wantFileType: "sdist",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "sdist with post version",
|
|
urlPath: "/packages/aa/bb/cc/package-1.0.0.post1.tar.gz",
|
|
wantName: "package",
|
|
wantVersion: "1.0.0.post1",
|
|
wantIsDownload: true,
|
|
wantFileType: "sdist",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "sdist with local version identifier",
|
|
urlPath: "/packages/dd/ee/ff/mylib-1.2.3+local.tar.gz",
|
|
wantName: "mylib",
|
|
wantVersion: "1.2.3+local",
|
|
wantIsDownload: true,
|
|
wantFileType: "sdist",
|
|
wantErr: false,
|
|
},
|
|
|
|
// Wheel files
|
|
{
|
|
name: "wheel simple package",
|
|
urlPath: "/packages/ab/cd/ef12/requests-2.28.0-py3-none-any.whl",
|
|
wantName: "requests",
|
|
wantVersion: "2.28.0",
|
|
wantIsDownload: true,
|
|
wantFileType: "wheel",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "wheel with platform-specific tags",
|
|
urlPath: "/packages/12/34/56/numpy-1.24.0-cp311-cp311-linux_x86_64.whl",
|
|
wantName: "numpy",
|
|
wantVersion: "1.24.0",
|
|
wantIsDownload: true,
|
|
wantFileType: "wheel",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "wheel with manylinux platform",
|
|
urlPath: "/packages/aa/bb/cc/cryptography-41.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
|
wantName: "cryptography",
|
|
wantVersion: "41.0.0",
|
|
wantIsDownload: true,
|
|
wantFileType: "wheel",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "wheel with underscore in name (normalized)",
|
|
urlPath: "/packages/11/22/33/some_package-1.0.0-py3-none-any.whl",
|
|
wantName: "some-package",
|
|
wantVersion: "1.0.0",
|
|
wantIsDownload: true,
|
|
wantFileType: "wheel",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "wheel with build tag",
|
|
urlPath: "/packages/ff/ee/dd/mypackage-1.0.0-1-py3-none-any.whl",
|
|
wantName: "mypackage",
|
|
wantVersion: "1.0.0",
|
|
wantIsDownload: true,
|
|
wantFileType: "wheel",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "wheel windows platform",
|
|
urlPath: "/packages/aa/bb/cc/pywin32-306-cp311-cp311-win_amd64.whl",
|
|
wantName: "pywin32",
|
|
wantVersion: "306",
|
|
wantIsDownload: true,
|
|
wantFileType: "wheel",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "wheel macos platform",
|
|
urlPath: "/packages/dd/ee/ff/tensorflow-2.15.0-cp311-cp311-macosx_10_15_x86_64.whl",
|
|
wantName: "tensorflow",
|
|
wantVersion: "2.15.0",
|
|
wantIsDownload: true,
|
|
wantFileType: "wheel",
|
|
wantErr: false,
|
|
},
|
|
|
|
// Real-world examples
|
|
{
|
|
name: "real django sdist",
|
|
urlPath: "/packages/b8/50/71e60c5e9148c20de37c37f3e4cd1da1f63f7d0f7ea4c7e9c8a2f5c8d9e1/Django-4.2.7.tar.gz",
|
|
wantName: "django",
|
|
wantVersion: "4.2.7",
|
|
wantIsDownload: true,
|
|
wantFileType: "sdist",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "real pandas wheel",
|
|
urlPath: "/packages/a1/b2/c3d4e5f6/pandas-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
|
wantName: "pandas",
|
|
wantVersion: "2.1.3",
|
|
wantIsDownload: true,
|
|
wantFileType: "wheel",
|
|
wantErr: false,
|
|
},
|
|
|
|
// Error cases
|
|
{
|
|
name: "empty URL path",
|
|
urlPath: "",
|
|
wantName: "",
|
|
wantVersion: "",
|
|
wantIsDownload: false,
|
|
wantFileType: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "just slash",
|
|
urlPath: "/",
|
|
wantName: "",
|
|
wantVersion: "",
|
|
wantIsDownload: false,
|
|
wantFileType: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid path without packages prefix",
|
|
urlPath: "/files/ab/cd/requests-2.28.0.tar.gz",
|
|
wantName: "",
|
|
wantVersion: "",
|
|
wantIsDownload: false,
|
|
wantFileType: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unsupported file type",
|
|
urlPath: "/packages/ab/cd/ef/readme.txt",
|
|
wantName: "",
|
|
wantVersion: "",
|
|
wantIsDownload: false,
|
|
wantFileType: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
parser := pypiFilesParser{}
|
|
got, err := parser.ParseURL(tt.urlPath)
|
|
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.wantName, got.GetName())
|
|
assert.Equal(t, tt.wantVersion, got.GetVersion())
|
|
assert.Equal(t, tt.wantIsDownload, got.IsFileDownload())
|
|
|
|
// Check file type via type assertion - must succeed for pypi packages
|
|
pypiInfo, ok := got.(*pypiPackageInfo)
|
|
assert.True(t, ok, "expected *pypiPackageInfo type")
|
|
if ok {
|
|
assert.Equal(t, tt.wantFileType, pypiInfo.FileType())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPypiOrgParser_ParseURL(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
urlPath string
|
|
wantName string
|
|
wantVersion string
|
|
wantIsDownload bool
|
|
wantErr bool
|
|
}{
|
|
// Simple API
|
|
{
|
|
name: "simple api package index",
|
|
urlPath: "/simple/requests/",
|
|
wantName: "requests",
|
|
wantVersion: "",
|
|
wantIsDownload: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "simple api package without trailing slash",
|
|
urlPath: "/simple/django",
|
|
wantName: "django",
|
|
wantVersion: "",
|
|
wantIsDownload: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "simple api normalized name",
|
|
urlPath: "/simple/flask-restful/",
|
|
wantName: "flask-restful",
|
|
wantVersion: "",
|
|
wantIsDownload: false,
|
|
wantErr: false,
|
|
},
|
|
|
|
// JSON API
|
|
{
|
|
name: "json api package metadata",
|
|
urlPath: "/pypi/requests/json",
|
|
wantName: "requests",
|
|
wantVersion: "",
|
|
wantIsDownload: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "json api version metadata",
|
|
urlPath: "/pypi/requests/2.28.0/json",
|
|
wantName: "requests",
|
|
wantVersion: "2.28.0",
|
|
wantIsDownload: false,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "json api with normalized name",
|
|
urlPath: "/pypi/flask-restful/0.3.10/json",
|
|
wantName: "flask-restful",
|
|
wantVersion: "0.3.10",
|
|
wantIsDownload: false,
|
|
wantErr: false,
|
|
},
|
|
|
|
// Error cases
|
|
{
|
|
name: "empty URL path",
|
|
urlPath: "",
|
|
wantName: "",
|
|
wantVersion: "",
|
|
wantIsDownload: false,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "just slash",
|
|
urlPath: "/",
|
|
wantName: "",
|
|
wantVersion: "",
|
|
wantIsDownload: false,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unknown path prefix",
|
|
urlPath: "/unknown/requests/",
|
|
wantName: "",
|
|
wantVersion: "",
|
|
wantIsDownload: false,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "simple api missing package name",
|
|
urlPath: "/simple/",
|
|
wantName: "",
|
|
wantVersion: "",
|
|
wantIsDownload: false,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "json api missing package name",
|
|
urlPath: "/pypi/json",
|
|
wantName: "",
|
|
wantVersion: "",
|
|
wantIsDownload: false,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
parser := pypiOrgParser{}
|
|
got, err := parser.ParseURL(tt.urlPath)
|
|
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.wantName, got.GetName())
|
|
assert.Equal(t, tt.wantVersion, got.GetVersion())
|
|
assert.Equal(t, tt.wantIsDownload, got.IsFileDownload())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseWheelFilename(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
filename string
|
|
wantName string
|
|
wantVersion string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "simple wheel",
|
|
filename: "requests-2.28.0-py3-none-any.whl",
|
|
wantName: "requests",
|
|
wantVersion: "2.28.0",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "wheel with cpython tag",
|
|
filename: "numpy-1.24.0-cp311-cp311-linux_x86_64.whl",
|
|
wantName: "numpy",
|
|
wantVersion: "1.24.0",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "wheel with build tag",
|
|
filename: "package-1.0.0-1-py3-none-any.whl",
|
|
wantName: "package",
|
|
wantVersion: "1.0.0",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "wheel with underscore name",
|
|
filename: "my_package-1.0.0-py3-none-any.whl",
|
|
wantName: "my-package",
|
|
wantVersion: "1.0.0",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid wheel - too few parts",
|
|
filename: "invalid-1.0.0.whl",
|
|
wantName: "",
|
|
wantVersion: "",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := parseWheelFilename(tt.filename)
|
|
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.wantName, got.GetName())
|
|
assert.Equal(t, tt.wantVersion, got.GetVersion())
|
|
assert.True(t, got.IsFileDownload())
|
|
assert.Equal(t, "wheel", got.FileType())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseSdistFilename(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
filename string
|
|
wantName string
|
|
wantVersion string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "simple tar.gz",
|
|
filename: "requests-2.28.0.tar.gz",
|
|
wantName: "requests",
|
|
wantVersion: "2.28.0",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "zip format",
|
|
filename: "django-4.2.0.zip",
|
|
wantName: "django",
|
|
wantVersion: "4.2.0",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "hyphenated name",
|
|
filename: "Flask-RESTful-0.3.10.tar.gz",
|
|
wantName: "flask-restful",
|
|
wantVersion: "0.3.10",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "prerelease version",
|
|
filename: "package-1.0.0rc1.tar.gz",
|
|
wantName: "package",
|
|
wantVersion: "1.0.0rc1",
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "dev version",
|
|
filename: "package-0.1.0.dev1.tar.gz",
|
|
wantName: "package",
|
|
wantVersion: "0.1.0.dev1",
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := parseSdistFilename(tt.filename)
|
|
|
|
if tt.wantErr {
|
|
assert.Error(t, err)
|
|
return
|
|
}
|
|
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, tt.wantName, got.GetName())
|
|
assert.Equal(t, tt.wantVersion, got.GetVersion())
|
|
assert.True(t, got.IsFileDownload())
|
|
assert.Equal(t, "sdist", got.FileType())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDenormalizePyPIPackageName(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"requests", "requests"},
|
|
{"Flask_RESTful", "flask-restful"},
|
|
{"My_Package", "my-package"},
|
|
{"UPPERCASE", "uppercase"},
|
|
{"under_score", "under-score"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
result := denormalizePyPIPackageName(tt.input)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetPypiRegistryConfigForHostname(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hostname string
|
|
expectConfig bool
|
|
expectHost string
|
|
}{
|
|
{
|
|
name: "exact match files.pythonhosted.org",
|
|
hostname: "files.pythonhosted.org",
|
|
expectConfig: true,
|
|
expectHost: "files.pythonhosted.org",
|
|
},
|
|
{
|
|
name: "exact match pypi.org",
|
|
hostname: "pypi.org",
|
|
expectConfig: true,
|
|
expectHost: "pypi.org",
|
|
},
|
|
{
|
|
name: "subdomain match",
|
|
hostname: "cdn.files.pythonhosted.org",
|
|
expectConfig: true,
|
|
expectHost: "files.pythonhosted.org",
|
|
},
|
|
{
|
|
name: "test pypi",
|
|
hostname: "test.pypi.org",
|
|
expectConfig: true,
|
|
expectHost: "test.pypi.org",
|
|
},
|
|
{
|
|
name: "unknown hostname",
|
|
hostname: "example.com",
|
|
expectConfig: false,
|
|
expectHost: "",
|
|
},
|
|
{
|
|
name: "partial match should not work",
|
|
hostname: "fakepypi.org",
|
|
expectConfig: false,
|
|
expectHost: "",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
config := pypiRegistryDomains.GetConfigForHostname(tt.hostname)
|
|
|
|
if !tt.expectConfig {
|
|
assert.Nil(t, config)
|
|
return
|
|
}
|
|
|
|
assert.NotNil(t, config)
|
|
assert.Equal(t, tt.expectHost, config.Host)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPypiRegistryDomains_ContainsHostname(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hostname string
|
|
want bool
|
|
}{
|
|
{
|
|
name: "exact match",
|
|
hostname: "files.pythonhosted.org",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "subdomain match",
|
|
hostname: "cdn.files.pythonhosted.org",
|
|
want: true,
|
|
},
|
|
{
|
|
name: "no match",
|
|
hostname: "example.com",
|
|
want: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := pypiRegistryDomains.ContainsHostname(tt.hostname)
|
|
assert.Equal(t, tt.want, got)
|
|
})
|
|
}
|
|
}
|