Metadata database tracks all uploads with project, package, tag, and timestamp queryable via API
This commit is contained in:
345
backend/tests/integration/test_packages_api.py
Normal file
345
backend/tests/integration/test_packages_api.py
Normal file
@@ -0,0 +1,345 @@
|
||||
"""
|
||||
Integration tests for package API endpoints.
|
||||
|
||||
Tests cover:
|
||||
- Package CRUD operations
|
||||
- Package listing with pagination, search, filtering
|
||||
- Package stats endpoint
|
||||
- Package-level audit logs
|
||||
- Cascade delete behavior
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from tests.factories import compute_sha256, upload_test_file
|
||||
|
||||
|
||||
class TestPackageCRUD:
|
||||
"""Tests for package create, read, update, delete operations."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_create_package(self, integration_client, test_project, unique_test_id):
|
||||
"""Test creating a new package."""
|
||||
package_name = f"test-create-pkg-{unique_test_id}"
|
||||
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{test_project}/packages",
|
||||
json={
|
||||
"name": package_name,
|
||||
"description": "Test package",
|
||||
"format": "npm",
|
||||
"platform": "linux",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert data["name"] == package_name
|
||||
assert data["description"] == "Test package"
|
||||
assert data["format"] == "npm"
|
||||
assert data["platform"] == "linux"
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_get_package(self, integration_client, test_package):
|
||||
"""Test getting a package by name."""
|
||||
project_name, package_name = test_package
|
||||
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{project_name}/packages/{package_name}"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert data["name"] == package_name
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_get_nonexistent_package(self, integration_client, test_project):
|
||||
"""Test getting a non-existent package returns 404."""
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{test_project}/packages/nonexistent-pkg"
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_list_packages(self, integration_client, test_package):
|
||||
"""Test listing packages includes created package."""
|
||||
project_name, package_name = test_package
|
||||
|
||||
response = integration_client.get(f"/api/v1/project/{project_name}/packages")
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert "pagination" in data
|
||||
|
||||
package_names = [p["name"] for p in data["items"]]
|
||||
assert package_name in package_names
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_delete_package(self, integration_client, test_project, unique_test_id):
|
||||
"""Test deleting a package."""
|
||||
package_name = f"test-delete-pkg-{unique_test_id}"
|
||||
|
||||
# Create package
|
||||
integration_client.post(
|
||||
f"/api/v1/project/{test_project}/packages",
|
||||
json={"name": package_name, "description": "To be deleted"},
|
||||
)
|
||||
|
||||
# Delete package
|
||||
response = integration_client.delete(
|
||||
f"/api/v1/project/{test_project}/packages/{package_name}"
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
# Verify deleted
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{test_project}/packages/{package_name}"
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestPackageListingFilters:
|
||||
"""Tests for package listing with filters and pagination."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_packages_pagination(self, integration_client, test_project):
|
||||
"""Test package listing respects pagination parameters."""
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{test_project}/packages?page=1&limit=5"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert len(data["items"]) <= 5
|
||||
assert data["pagination"]["limit"] == 5
|
||||
assert data["pagination"]["page"] == 1
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_packages_filter_by_format(
|
||||
self, integration_client, test_project, unique_test_id
|
||||
):
|
||||
"""Test package filtering by format."""
|
||||
# Create a package with specific format
|
||||
package_name = f"npm-pkg-{unique_test_id}"
|
||||
integration_client.post(
|
||||
f"/api/v1/project/{test_project}/packages",
|
||||
json={"name": package_name, "format": "npm"},
|
||||
)
|
||||
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{test_project}/packages?format=npm"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
for pkg in data["items"]:
|
||||
assert pkg["format"] == "npm"
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_packages_filter_by_platform(
|
||||
self, integration_client, test_project, unique_test_id
|
||||
):
|
||||
"""Test package filtering by platform."""
|
||||
# Create a package with specific platform
|
||||
package_name = f"linux-pkg-{unique_test_id}"
|
||||
integration_client.post(
|
||||
f"/api/v1/project/{test_project}/packages",
|
||||
json={"name": package_name, "platform": "linux"},
|
||||
)
|
||||
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{test_project}/packages?platform=linux"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
for pkg in data["items"]:
|
||||
assert pkg["platform"] == "linux"
|
||||
|
||||
|
||||
class TestPackageStats:
|
||||
"""Tests for package statistics endpoint."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_package_stats_returns_valid_response(
|
||||
self, integration_client, test_package
|
||||
):
|
||||
"""Test package stats endpoint returns expected fields."""
|
||||
project, package = test_package
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{project}/packages/{package}/stats"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "package_id" in data
|
||||
assert "package_name" in data
|
||||
assert "project_name" in data
|
||||
assert "tag_count" in data
|
||||
assert "artifact_count" in data
|
||||
assert "total_size_bytes" in data
|
||||
assert "upload_count" in data
|
||||
assert "deduplicated_uploads" in data
|
||||
assert "storage_saved_bytes" in data
|
||||
assert "deduplication_ratio" in data
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_package_stats_not_found(self, integration_client, test_project):
|
||||
"""Test package stats returns 404 for non-existent package."""
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{test_project}/packages/nonexistent-package/stats"
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestPackageAuditLogs:
|
||||
"""Tests for package-level audit logs endpoint."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_package_audit_logs_returns_200(self, integration_client, test_package):
|
||||
"""Test package audit logs endpoint returns 200."""
|
||||
project_name, package_name = test_package
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{project_name}/{package_name}/audit-logs"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert "pagination" in data
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_package_audit_logs_project_not_found(self, integration_client):
|
||||
"""Test non-existent project returns 404."""
|
||||
response = integration_client.get(
|
||||
"/api/v1/project/nonexistent/nonexistent/audit-logs"
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_package_audit_logs_package_not_found(
|
||||
self, integration_client, test_project
|
||||
):
|
||||
"""Test non-existent package returns 404."""
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{test_project}/nonexistent-package/audit-logs"
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestPackageCascadeDelete:
|
||||
"""Tests for cascade delete behavior when deleting packages."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_ref_count_decrements_on_package_delete(
|
||||
self, integration_client, unique_test_id
|
||||
):
|
||||
"""Test ref_count decrements for all tags when package is deleted."""
|
||||
project_name = f"cascade-pkg-{unique_test_id}"
|
||||
package_name = f"test-pkg-{unique_test_id}"
|
||||
|
||||
# Create project
|
||||
response = integration_client.post(
|
||||
"/api/v1/projects",
|
||||
json={
|
||||
"name": project_name,
|
||||
"description": "Test project",
|
||||
"is_public": True,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Create package
|
||||
response = integration_client.post(
|
||||
f"/api/v1/project/{project_name}/packages",
|
||||
json={"name": package_name, "description": "Test package"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Upload content with multiple tags
|
||||
content = f"cascade delete test {unique_test_id}".encode()
|
||||
expected_hash = compute_sha256(content)
|
||||
|
||||
upload_test_file(
|
||||
integration_client, project_name, package_name, content, tag="v1"
|
||||
)
|
||||
upload_test_file(
|
||||
integration_client, project_name, package_name, content, tag="v2"
|
||||
)
|
||||
upload_test_file(
|
||||
integration_client, project_name, package_name, content, tag="v3"
|
||||
)
|
||||
|
||||
# Verify ref_count is 3
|
||||
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
||||
assert response.json()["ref_count"] == 3
|
||||
|
||||
# Delete the package
|
||||
delete_response = integration_client.delete(
|
||||
f"/api/v1/project/{project_name}/packages/{package_name}"
|
||||
)
|
||||
assert delete_response.status_code == 204
|
||||
|
||||
# Verify ref_count is 0
|
||||
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
||||
assert response.json()["ref_count"] == 0
|
||||
|
||||
# Cleanup
|
||||
integration_client.delete(f"/api/v1/projects/{project_name}")
|
||||
|
||||
|
||||
class TestPackageUploads:
|
||||
"""Tests for package-level uploads endpoint."""
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_package_uploads_returns_200(self, integration_client, test_package):
|
||||
"""Test package uploads endpoint returns 200."""
|
||||
project_name, package_name = test_package
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{project_name}/{package_name}/uploads"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert "pagination" in data
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_package_uploads_after_upload(self, integration_client, test_package):
|
||||
"""Test uploads are recorded after file upload."""
|
||||
project_name, package_name = test_package
|
||||
|
||||
# Upload a file
|
||||
upload_result = upload_test_file(
|
||||
integration_client,
|
||||
project_name,
|
||||
package_name,
|
||||
b"test upload content",
|
||||
"test.txt",
|
||||
)
|
||||
assert upload_result["artifact_id"]
|
||||
|
||||
# Check uploads endpoint
|
||||
response = integration_client.get(
|
||||
f"/api/v1/project/{project_name}/{package_name}/uploads"
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
data = response.json()
|
||||
assert len(data["items"]) >= 1
|
||||
|
||||
# Verify upload record fields
|
||||
upload = data["items"][0]
|
||||
assert "artifact_id" in upload
|
||||
assert "package_name" in upload
|
||||
assert "project_name" in upload
|
||||
assert "uploaded_at" in upload
|
||||
assert "uploaded_by" in upload
|
||||
|
||||
@pytest.mark.integration
|
||||
def test_package_uploads_project_not_found(self, integration_client):
|
||||
"""Test non-existent project returns 404."""
|
||||
response = integration_client.get(
|
||||
"/api/v1/project/nonexistent/nonexistent/uploads"
|
||||
)
|
||||
assert response.status_code == 404
|
||||
Reference in New Issue
Block a user