Fix httpx.Timeout configuration in PyPI proxy
This commit is contained in:
@@ -47,7 +47,7 @@ class TestUploadBasics:
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
result = upload_test_file(
|
||||
integration_client, project_name, package_name, content, tag="v1"
|
||||
integration_client, project_name, package_name, content, version="v1"
|
||||
)
|
||||
|
||||
assert result["artifact_id"] == expected_hash
|
||||
@@ -116,31 +116,23 @@ class TestUploadBasics:
|
||||
assert result["created_at"] is not None
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_upload_without_tag_succeeds(self, integration_client, test_package):
|
||||
"""Test upload without tag succeeds (no tag created)."""
|
||||
def test_upload_without_version_succeeds(self, integration_client, test_package):
|
||||
"""Test upload without version succeeds (no version created)."""
|
||||
project, package = test_package
|
||||
content = b"upload without tag test"
|
||||
content = b"upload without version test"
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
files = {"file": ("no_tag.bin", io.BytesIO(content), "application/octet-stream")}
|
||||
files = {"file": ("no_version.bin", io.BytesIO(content), "application/octet-stream")}
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
files=files,
|
||||
# No tag parameter
|
||||
# No version parameter
|
||||
)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["artifact_id"] == expected_hash
|
||||
|
||||
# Verify no tag was created - list tags and check
|
||||
tags_response = integration_client.get(
|
||||
f"/api/v1/project/{project}/{package}/tags"
|
||||
)
|
||||
assert tags_response.status_code == 200
|
||||
tags = tags_response.json()
|
||||
# Filter for tags pointing to this artifact
|
||||
artifact_tags = [t for t in tags.get("items", tags) if t.get("artifact_id") == expected_hash]
|
||||
assert len(artifact_tags) == 0, "Tag should not be created when not specified"
|
||||
# Version should be None when not specified
|
||||
assert result.get("version") is None
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_upload_creates_artifact_in_database(self, integration_client, test_package):
|
||||
@@ -172,25 +164,29 @@ class TestUploadBasics:
|
||||
assert s3_object_exists(expected_hash), "S3 object should exist after upload"
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_upload_with_tag_creates_tag_record(self, integration_client, test_package):
|
||||
"""Test upload with tag creates tag record."""
|
||||
def test_upload_with_version_creates_version_record(self, integration_client, test_package):
|
||||
"""Test upload with version creates version record."""
|
||||
project, package = test_package
|
||||
content = b"tag creation test"
|
||||
content = b"version creation test"
|
||||
expected_hash = compute_sha256(content)
|
||||
tag_name = "my-tag-v1"
|
||||
version_name = "1.0.0"
|
||||
|
||||
upload_test_file(
|
||||
integration_client, project, package, content, tag=tag_name
|
||||
result = upload_test_file(
|
||||
integration_client, project, package, content, version=version_name
|
||||
)
|
||||
|
||||
# Verify tag exists
|
||||
tags_response = integration_client.get(
|
||||
f"/api/v1/project/{project}/{package}/tags"
|
||||
# Verify version was created
|
||||
assert result.get("version") == version_name
|
||||
assert result["artifact_id"] == expected_hash
|
||||
|
||||
# Verify version exists in versions list
|
||||
versions_response = integration_client.get(
|
||||
f"/api/v1/project/{project}/{package}/versions"
|
||||
)
|
||||
assert tags_response.status_code == 200
|
||||
tags = tags_response.json()
|
||||
tag_names = [t["name"] for t in tags.get("items", tags)]
|
||||
assert tag_name in tag_names
|
||||
assert versions_response.status_code == 200
|
||||
versions = versions_response.json()
|
||||
version_names = [v["version"] for v in versions.get("items", [])]
|
||||
assert version_name in version_names
|
||||
|
||||
|
||||
class TestDuplicateUploads:
|
||||
@@ -207,36 +203,44 @@ class TestDuplicateUploads:
|
||||
|
||||
# First upload
|
||||
result1 = upload_test_file(
|
||||
integration_client, project, package, content, tag="first"
|
||||
integration_client, project, package, content, version="first"
|
||||
)
|
||||
assert result1["artifact_id"] == expected_hash
|
||||
|
||||
# Second upload
|
||||
result2 = upload_test_file(
|
||||
integration_client, project, package, content, tag="second"
|
||||
integration_client, project, package, content, version="second"
|
||||
)
|
||||
assert result2["artifact_id"] == expected_hash
|
||||
assert result1["artifact_id"] == result2["artifact_id"]
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_same_file_twice_increments_ref_count(
|
||||
def test_same_file_twice_returns_existing_version(
|
||||
self, integration_client, test_package
|
||||
):
|
||||
"""Test uploading same file twice increments ref_count to 2."""
|
||||
"""Test uploading same file twice in same package returns existing version.
|
||||
|
||||
Same artifact can only have one version per package. Uploading the same content
|
||||
with a different version name returns the existing version, not a new one.
|
||||
ref_count stays at 1 because there's still only one PackageVersion reference.
|
||||
"""
|
||||
project, package = test_package
|
||||
content = b"content for ref count increment test"
|
||||
|
||||
# First upload
|
||||
result1 = upload_test_file(
|
||||
integration_client, project, package, content, tag="v1"
|
||||
integration_client, project, package, content, version="v1"
|
||||
)
|
||||
assert result1["ref_count"] == 1
|
||||
|
||||
# Second upload
|
||||
# Second upload with different version name returns existing version
|
||||
result2 = upload_test_file(
|
||||
integration_client, project, package, content, tag="v2"
|
||||
integration_client, project, package, content, version="v2"
|
||||
)
|
||||
assert result2["ref_count"] == 2
|
||||
# Same artifact, same package = same version returned, ref_count stays 1
|
||||
assert result2["ref_count"] == 1
|
||||
assert result2["deduplicated"] is True
|
||||
assert result1["version"] == result2["version"] # Both return "v1"
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_same_file_different_packages_shares_artifact(
|
||||
@@ -261,12 +265,12 @@ class TestDuplicateUploads:
|
||||
)
|
||||
|
||||
# Upload to first package
|
||||
result1 = upload_test_file(integration_client, project, pkg1, content, tag="v1")
|
||||
result1 = upload_test_file(integration_client, project, pkg1, content, version="v1")
|
||||
assert result1["artifact_id"] == expected_hash
|
||||
assert result1["deduplicated"] is False
|
||||
|
||||
# Upload to second package
|
||||
result2 = upload_test_file(integration_client, project, pkg2, content, tag="v1")
|
||||
result2 = upload_test_file(integration_client, project, pkg2, content, version="v1")
|
||||
assert result2["artifact_id"] == expected_hash
|
||||
assert result2["deduplicated"] is True
|
||||
|
||||
@@ -286,7 +290,7 @@ class TestDuplicateUploads:
|
||||
package,
|
||||
content,
|
||||
filename="file1.bin",
|
||||
tag="v1",
|
||||
version="v1",
|
||||
)
|
||||
assert result1["artifact_id"] == expected_hash
|
||||
|
||||
@@ -297,7 +301,7 @@ class TestDuplicateUploads:
|
||||
package,
|
||||
content,
|
||||
filename="file2.bin",
|
||||
tag="v2",
|
||||
version="v2",
|
||||
)
|
||||
assert result2["artifact_id"] == expected_hash
|
||||
assert result2["deduplicated"] is True
|
||||
@@ -307,17 +311,17 @@ class TestDownload:
|
||||
"""Tests for download functionality."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_download_by_tag(self, integration_client, test_package):
|
||||
"""Test downloading artifact by tag name."""
|
||||
def test_download_by_version(self, integration_client, test_package):
|
||||
"""Test downloading artifact by version."""
|
||||
project, package = test_package
|
||||
original_content = b"download by tag test"
|
||||
original_content = b"download by version test"
|
||||
|
||||
upload_test_file(
|
||||
integration_client, project, package, original_content, tag="download-tag"
|
||||
integration_client, project, package, original_content, version="1.0.0"
|
||||
)
|
||||
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{project}/{package}/+/download-tag",
|
||||
f"/api/v1/project/{project}/{package}/+/1.0.0",
|
||||
params={"mode": "proxy"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -340,29 +344,29 @@ class TestDownload:
|
||||
assert response.content == original_content
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_download_by_tag_prefix(self, integration_client, test_package):
|
||||
"""Test downloading artifact using tag: prefix."""
|
||||
def test_download_by_version_prefix(self, integration_client, test_package):
|
||||
"""Test downloading artifact using version: prefix."""
|
||||
project, package = test_package
|
||||
original_content = b"download by tag prefix test"
|
||||
original_content = b"download by version prefix test"
|
||||
|
||||
upload_test_file(
|
||||
integration_client, project, package, original_content, tag="prefix-tag"
|
||||
integration_client, project, package, original_content, version="2.0.0"
|
||||
)
|
||||
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{project}/{package}/+/tag:prefix-tag",
|
||||
f"/api/v1/project/{project}/{package}/+/version:2.0.0",
|
||||
params={"mode": "proxy"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.content == original_content
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_download_nonexistent_tag(self, integration_client, test_package):
|
||||
"""Test downloading nonexistent tag returns 404."""
|
||||
def test_download_nonexistent_version(self, integration_client, test_package):
|
||||
"""Test downloading nonexistent version returns 404."""
|
||||
project, package = test_package
|
||||
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{project}/{package}/+/nonexistent-tag"
|
||||
f"/api/v1/project/{project}/{package}/+/nonexistent-version"
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
@@ -400,7 +404,7 @@ class TestDownload:
|
||||
original_content = b"exact content verification test data 12345"
|
||||
|
||||
upload_test_file(
|
||||
integration_client, project, package, original_content, tag="verify"
|
||||
integration_client, project, package, original_content, version="verify"
|
||||
)
|
||||
|
||||
response = integration_client.get(
|
||||
@@ -421,7 +425,7 @@ class TestDownloadHeaders:
|
||||
|
||||
upload_test_file(
|
||||
integration_client, project, package, content,
|
||||
filename="test.txt", tag="content-type-test"
|
||||
filename="test.txt", version="content-type-test"
|
||||
)
|
||||
|
||||
response = integration_client.get(
|
||||
@@ -440,7 +444,7 @@ class TestDownloadHeaders:
|
||||
expected_length = len(content)
|
||||
|
||||
upload_test_file(
|
||||
integration_client, project, package, content, tag="content-length-test"
|
||||
integration_client, project, package, content, version="content-length-test"
|
||||
)
|
||||
|
||||
response = integration_client.get(
|
||||
@@ -460,7 +464,7 @@ class TestDownloadHeaders:
|
||||
|
||||
upload_test_file(
|
||||
integration_client, project, package, content,
|
||||
filename=filename, tag="disposition-test"
|
||||
filename=filename, version="disposition-test"
|
||||
)
|
||||
|
||||
response = integration_client.get(
|
||||
@@ -481,7 +485,7 @@ class TestDownloadHeaders:
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
upload_test_file(
|
||||
integration_client, project, package, content, tag="checksum-headers"
|
||||
integration_client, project, package, content, version="checksum-headers"
|
||||
)
|
||||
|
||||
response = integration_client.get(
|
||||
@@ -501,7 +505,7 @@ class TestDownloadHeaders:
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
upload_test_file(
|
||||
integration_client, project, package, content, tag="etag-test"
|
||||
integration_client, project, package, content, version="etag-test"
|
||||
)
|
||||
|
||||
response = integration_client.get(
|
||||
@@ -519,17 +523,31 @@ class TestConcurrentUploads:
|
||||
"""Tests for concurrent upload handling."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_concurrent_uploads_same_file(self, integration_client, test_package):
|
||||
"""Test concurrent uploads of same file handle deduplication correctly."""
|
||||
project, package = test_package
|
||||
def test_concurrent_uploads_same_file(self, integration_client, test_project, unique_test_id):
|
||||
"""Test concurrent uploads of same file to different packages handle deduplication correctly.
|
||||
|
||||
Same artifact can only have one version per package, so we create multiple packages
|
||||
to test that concurrent uploads to different packages correctly increment ref_count.
|
||||
"""
|
||||
content = b"content for concurrent upload test"
|
||||
expected_hash = compute_sha256(content)
|
||||
num_concurrent = 5
|
||||
|
||||
# Create packages for each concurrent upload
|
||||
packages = []
|
||||
for i in range(num_concurrent):
|
||||
pkg_name = f"concurrent-pkg-{unique_test_id}-{i}"
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{test_project}/packages",
|
||||
json={"name": pkg_name},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
packages.append(pkg_name)
|
||||
|
||||
# Create an API key for worker threads
|
||||
api_key_response = integration_client.post(
|
||||
"/api/v1/auth/keys",
|
||||
json={"name": "concurrent-test-key"},
|
||||
json={"name": f"concurrent-test-key-{unique_test_id}"},
|
||||
)
|
||||
assert api_key_response.status_code == 200, f"Failed to create API key: {api_key_response.text}"
|
||||
api_key = api_key_response.json()["key"]
|
||||
@@ -537,7 +555,7 @@ class TestConcurrentUploads:
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
def upload_worker(tag_suffix):
|
||||
def upload_worker(idx):
|
||||
try:
|
||||
from httpx import Client
|
||||
|
||||
@@ -545,15 +563,15 @@ class TestConcurrentUploads:
|
||||
with Client(base_url=base_url, timeout=30.0) as client:
|
||||
files = {
|
||||
"file": (
|
||||
f"concurrent-{tag_suffix}.bin",
|
||||
f"concurrent-{idx}.bin",
|
||||
io.BytesIO(content),
|
||||
"application/octet-stream",
|
||||
)
|
||||
}
|
||||
response = client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
f"/api/v1/project/{test_project}/{packages[idx]}/upload",
|
||||
files=files,
|
||||
data={"tag": f"concurrent-{tag_suffix}"},
|
||||
data={"version": "1.0.0"},
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
)
|
||||
if response.status_code == 200:
|
||||
@@ -576,7 +594,7 @@ class TestConcurrentUploads:
|
||||
assert len(artifact_ids) == 1
|
||||
assert expected_hash in artifact_ids
|
||||
|
||||
# Verify final ref_count
|
||||
# Verify final ref_count equals number of packages
|
||||
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
||||
assert response.status_code == 200
|
||||
assert response.json()["ref_count"] == num_concurrent
|
||||
@@ -605,7 +623,7 @@ class TestFileSizeValidation:
|
||||
content = b"X"
|
||||
|
||||
result = upload_test_file(
|
||||
integration_client, project, package, content, tag="tiny"
|
||||
integration_client, project, package, content, version="tiny"
|
||||
)
|
||||
|
||||
assert result["artifact_id"] is not None
|
||||
@@ -621,7 +639,7 @@ class TestFileSizeValidation:
|
||||
expected_size = len(content)
|
||||
|
||||
result = upload_test_file(
|
||||
integration_client, project, package, content, tag="size-test"
|
||||
integration_client, project, package, content, version="size-test"
|
||||
)
|
||||
|
||||
assert result["size"] == expected_size
|
||||
@@ -649,7 +667,7 @@ class TestUploadFailureCleanup:
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/nonexistent-project-{unique_test_id}/nonexistent-pkg/upload",
|
||||
files=files,
|
||||
data={"tag": "test"},
|
||||
data={"version": "test"},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
@@ -672,7 +690,7 @@ class TestUploadFailureCleanup:
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{test_project}/nonexistent-package-{unique_test_id}/upload",
|
||||
files=files,
|
||||
data={"tag": "test"},
|
||||
data={"version": "test"},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
@@ -693,7 +711,7 @@ class TestUploadFailureCleanup:
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{test_project}/nonexistent-package-{unique_test_id}/upload",
|
||||
files=files,
|
||||
data={"tag": "test"},
|
||||
data={"version": "test"},
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
@@ -719,7 +737,7 @@ class TestS3StorageVerification:
|
||||
|
||||
# Upload same content multiple times
|
||||
for tag in ["s3test1", "s3test2", "s3test3"]:
|
||||
upload_test_file(integration_client, project, package, content, tag=tag)
|
||||
upload_test_file(integration_client, project, package, content, version=tag)
|
||||
|
||||
# Verify only one S3 object exists
|
||||
s3_objects = list_s3_objects_by_hash(expected_hash)
|
||||
@@ -735,16 +753,26 @@ class TestS3StorageVerification:
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_artifact_table_single_row_after_duplicates(
|
||||
self, integration_client, test_package
|
||||
self, integration_client, test_project, unique_test_id
|
||||
):
|
||||
"""Test artifact table contains only one row after duplicate uploads."""
|
||||
project, package = test_package
|
||||
"""Test artifact table contains only one row after duplicate uploads to different packages.
|
||||
|
||||
Same artifact can only have one version per package, so we create multiple packages
|
||||
to test deduplication across packages.
|
||||
"""
|
||||
content = b"content for single row test"
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
# Upload same content multiple times
|
||||
for tag in ["v1", "v2", "v3"]:
|
||||
upload_test_file(integration_client, project, package, content, tag=tag)
|
||||
# Create 3 packages and upload same content to each
|
||||
for i in range(3):
|
||||
pkg_name = f"single-row-pkg-{unique_test_id}-{i}"
|
||||
integration_client.post(
|
||||
f"/api/v1/project/{test_project}/packages",
|
||||
json={"name": pkg_name},
|
||||
)
|
||||
upload_test_file(
|
||||
integration_client, test_project, pkg_name, content, version="1.0.0"
|
||||
)
|
||||
|
||||
# Query artifact
|
||||
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
||||
@@ -783,7 +811,7 @@ class TestSecurityPathTraversal:
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
files=files,
|
||||
data={"tag": "traversal-test"},
|
||||
data={"version": "traversal-test"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
@@ -801,48 +829,16 @@ class TestSecurityPathTraversal:
|
||||
assert response.status_code in [400, 404, 422]
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_path_traversal_in_tag_name(self, integration_client, test_package):
|
||||
"""Test tag names with path traversal are handled safely."""
|
||||
def test_path_traversal_in_version_name(self, integration_client, test_package):
|
||||
"""Test version names with path traversal are handled safely."""
|
||||
project, package = test_package
|
||||
content = b"tag traversal test"
|
||||
content = b"version traversal test"
|
||||
|
||||
files = {"file": ("test.bin", io.BytesIO(content), "application/octet-stream")}
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
files=files,
|
||||
data={"tag": "../../../etc/passwd"},
|
||||
)
|
||||
assert response.status_code in [200, 400, 422]
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_download_path_traversal_in_ref(self, integration_client, test_package):
|
||||
"""Test download ref with path traversal is rejected."""
|
||||
project, package = test_package
|
||||
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{project}/{package}/+/../../../etc/passwd"
|
||||
)
|
||||
assert response.status_code in [400, 404, 422]
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_path_traversal_in_package_name(self, integration_client, test_project):
|
||||
"""Test package names with path traversal sequences are rejected."""
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{test_project}/packages/../../../etc/passwd"
|
||||
)
|
||||
assert response.status_code in [400, 404, 422]
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_path_traversal_in_tag_name(self, integration_client, test_package):
|
||||
"""Test tag names with path traversal are rejected or handled safely."""
|
||||
project, package = test_package
|
||||
content = b"tag traversal test"
|
||||
|
||||
files = {"file": ("test.bin", io.BytesIO(content), "application/octet-stream")}
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
files=files,
|
||||
data={"tag": "../../../etc/passwd"},
|
||||
data={"version": "../../../etc/passwd"},
|
||||
)
|
||||
assert response.status_code in [200, 400, 422]
|
||||
|
||||
@@ -867,7 +863,7 @@ class TestSecurityMalformedRequests:
|
||||
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project}/{package}/upload",
|
||||
data={"tag": "no-file"},
|
||||
data={"version": "no-file"},
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
|
||||
Reference in New Issue
Block a user