- Update CacheRequest test to use version field - Fix upload_test_file calls that still used tag parameter - Update artifact history test to check versions instead of tags - Update artifact stats tests to check versions instead of tags - Fix garbage collection tests to delete versions instead of tags - Remove TestGlobalTags class (endpoint removed) - Update project/package stats tests to check version_count - Fix upload_test_file fixture in test_download_verification
328 lines
12 KiB
Python
328 lines
12 KiB
Python
"""
|
|
Integration tests for project API endpoints.
|
|
|
|
Tests cover:
|
|
- Project CRUD operations
|
|
- Project listing with pagination, search, and sorting
|
|
- Project stats endpoint
|
|
- Project-level audit logs
|
|
- Cascade delete behavior
|
|
"""
|
|
|
|
import pytest
|
|
from tests.factories import compute_sha256, upload_test_file
|
|
|
|
|
|
class TestProjectCRUD:
|
|
"""Tests for project create, read, update, delete operations."""
|
|
|
|
@pytest.mark.integration
|
|
def test_create_project(self, integration_client, unique_test_id):
|
|
"""Test creating a new project."""
|
|
project_name = f"test-create-{unique_test_id}"
|
|
|
|
try:
|
|
response = integration_client.post(
|
|
"/api/v1/projects",
|
|
json={
|
|
"name": project_name,
|
|
"description": "Test project",
|
|
"is_public": True,
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert data["name"] == project_name
|
|
assert data["description"] == "Test project"
|
|
assert data["is_public"] is True
|
|
assert "id" in data
|
|
assert "created_at" in data
|
|
finally:
|
|
integration_client.delete(f"/api/v1/projects/{project_name}")
|
|
|
|
@pytest.mark.integration
|
|
def test_get_project(self, integration_client, test_project):
|
|
"""Test getting a project by name."""
|
|
response = integration_client.get(f"/api/v1/projects/{test_project}")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert data["name"] == test_project
|
|
|
|
@pytest.mark.integration
|
|
def test_get_nonexistent_project(self, integration_client):
|
|
"""Test getting a non-existent project returns 404."""
|
|
response = integration_client.get("/api/v1/projects/nonexistent-project-xyz")
|
|
assert response.status_code == 404
|
|
|
|
@pytest.mark.integration
|
|
def test_list_projects(self, integration_client, test_project):
|
|
"""Test listing projects includes created project."""
|
|
# Search specifically for our test project to avoid pagination issues
|
|
response = integration_client.get(f"/api/v1/projects?search={test_project}")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert "pagination" in data
|
|
|
|
project_names = [p["name"] for p in data["items"]]
|
|
assert test_project in project_names
|
|
|
|
@pytest.mark.integration
|
|
def test_delete_project(self, integration_client, unique_test_id):
|
|
"""Test deleting a project."""
|
|
project_name = f"test-delete-{unique_test_id}"
|
|
|
|
# Create project
|
|
integration_client.post(
|
|
"/api/v1/projects",
|
|
json={"name": project_name, "description": "To be deleted"},
|
|
)
|
|
|
|
# Delete project
|
|
response = integration_client.delete(f"/api/v1/projects/{project_name}")
|
|
assert response.status_code == 204
|
|
|
|
# Verify deleted
|
|
response = integration_client.get(f"/api/v1/projects/{project_name}")
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestProjectListingFilters:
|
|
"""Tests for project listing with filters and pagination."""
|
|
|
|
@pytest.mark.integration
|
|
def test_projects_pagination(self, integration_client):
|
|
"""Test project listing respects pagination parameters."""
|
|
response = integration_client.get("/api/v1/projects?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
|
|
assert "has_more" in data["pagination"]
|
|
|
|
@pytest.mark.integration
|
|
def test_projects_search(self, integration_client, test_project):
|
|
"""Test project search by name."""
|
|
# Search using the unique portion of our test project name
|
|
# test_project format is "test-project-test-{uuid[:8]}"
|
|
unique_part = test_project.split("-")[-1] # Get the UUID portion
|
|
response = integration_client.get(
|
|
f"/api/v1/projects?search={unique_part}"
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
# Our project should be in results
|
|
project_names = [p["name"] for p in data["items"]]
|
|
assert test_project in project_names
|
|
|
|
@pytest.mark.integration
|
|
def test_projects_sort_by_name(self, integration_client):
|
|
"""Test project sorting by name."""
|
|
response = integration_client.get("/api/v1/projects?sort=name&order=asc")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
# Filter out system projects (names starting with "_") as they may have
|
|
# collation-specific sort behavior and aren't part of the test data
|
|
names = [p["name"] for p in data["items"] if not p["name"].startswith("_")]
|
|
assert names == sorted(names)
|
|
|
|
|
|
class TestProjectStats:
|
|
"""Tests for project statistics endpoint."""
|
|
|
|
@pytest.mark.integration
|
|
def test_project_stats_returns_valid_response(
|
|
self, integration_client, test_project
|
|
):
|
|
"""Test project stats endpoint returns expected fields."""
|
|
response = integration_client.get(f"/api/v1/projects/{test_project}/stats")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert "project_id" in data
|
|
assert "project_name" in data
|
|
assert "package_count" in data
|
|
assert "version_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_project_stats_not_found(self, integration_client):
|
|
"""Test project stats returns 404 for non-existent project."""
|
|
response = integration_client.get("/api/v1/projects/nonexistent-project/stats")
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestProjectAuditLogs:
|
|
"""Tests for project-level audit logs endpoint."""
|
|
|
|
@pytest.mark.integration
|
|
def test_project_audit_logs_returns_200(self, integration_client, test_project):
|
|
"""Test project audit logs endpoint returns 200."""
|
|
response = integration_client.get(f"/api/v1/projects/{test_project}/audit-logs")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert "pagination" in data
|
|
|
|
@pytest.mark.integration
|
|
def test_project_audit_logs_not_found(self, integration_client):
|
|
"""Test non-existent project returns 404."""
|
|
response = integration_client.get(
|
|
"/api/v1/projects/nonexistent-project/audit-logs"
|
|
)
|
|
assert response.status_code == 404
|
|
|
|
|
|
class TestProjectCascadeDelete:
|
|
"""Tests for cascade delete behavior when deleting projects."""
|
|
|
|
@pytest.mark.integration
|
|
def test_project_delete_cascades_to_packages(
|
|
self, integration_client, unique_test_id
|
|
):
|
|
"""Test deleting project cascades to packages."""
|
|
project_name = f"cascade-proj-{unique_test_id}"
|
|
package_name = f"cascade-pkg-{unique_test_id}"
|
|
|
|
try:
|
|
# Create project and package
|
|
integration_client.post(
|
|
"/api/v1/projects",
|
|
json={"name": project_name, "description": "Test", "is_public": True},
|
|
)
|
|
integration_client.post(
|
|
f"/api/v1/project/{project_name}/packages",
|
|
json={"name": package_name, "description": "Test package"},
|
|
)
|
|
|
|
# Verify package exists
|
|
response = integration_client.get(
|
|
f"/api/v1/project/{project_name}/packages/{package_name}"
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# Delete project
|
|
integration_client.delete(f"/api/v1/projects/{project_name}")
|
|
|
|
# Verify project is deleted (and package with it)
|
|
response = integration_client.get(f"/api/v1/projects/{project_name}")
|
|
assert response.status_code == 404
|
|
except Exception:
|
|
# Cleanup if test fails
|
|
integration_client.delete(f"/api/v1/projects/{project_name}")
|
|
raise
|
|
|
|
@pytest.mark.integration
|
|
def test_ref_count_decrements_on_project_delete(
|
|
self, integration_client, unique_test_id
|
|
):
|
|
"""Test ref_count decrements for all tags when project is deleted."""
|
|
project_name = f"cascade-proj-{unique_test_id}"
|
|
package1_name = f"pkg1-{unique_test_id}"
|
|
package2_name = f"pkg2-{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 two packages
|
|
for pkg_name in [package1_name, package2_name]:
|
|
response = integration_client.post(
|
|
f"/api/v1/project/{project_name}/packages",
|
|
json={"name": pkg_name, "description": "Test package"},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# Upload same content with tags in both packages
|
|
content = f"project cascade test {unique_test_id}".encode()
|
|
expected_hash = compute_sha256(content)
|
|
|
|
upload_test_file(
|
|
integration_client, project_name, package1_name, content, version="v1"
|
|
)
|
|
upload_test_file(
|
|
integration_client, project_name, package1_name, content, version="v2"
|
|
)
|
|
upload_test_file(
|
|
integration_client, project_name, package2_name, content, version="latest"
|
|
)
|
|
upload_test_file(
|
|
integration_client, project_name, package2_name, content, version="stable"
|
|
)
|
|
|
|
# Verify ref_count is 4 (2 tags in each of 2 packages)
|
|
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
|
assert response.json()["ref_count"] == 4
|
|
|
|
# Delete the project
|
|
delete_response = integration_client.delete(f"/api/v1/projects/{project_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
|
|
|
|
|
|
class TestProjectUploads:
|
|
"""Tests for project-level uploads endpoint."""
|
|
|
|
@pytest.mark.integration
|
|
def test_project_uploads_returns_200(self, integration_client, test_project):
|
|
"""Test project uploads endpoint returns 200."""
|
|
response = integration_client.get(f"/api/v1/project/{test_project}/uploads")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert "items" in data
|
|
assert "pagination" in data
|
|
|
|
@pytest.mark.integration
|
|
def test_project_uploads_after_upload(self, integration_client, test_package):
|
|
"""Test uploads are recorded in project uploads."""
|
|
project_name, package_name = test_package
|
|
|
|
# Upload a file
|
|
upload_test_file(
|
|
integration_client,
|
|
project_name,
|
|
package_name,
|
|
b"project uploads test",
|
|
"project.txt",
|
|
)
|
|
|
|
response = integration_client.get(f"/api/v1/project/{project_name}/uploads")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert len(data["items"]) >= 1
|
|
|
|
# Verify project name matches
|
|
for item in data["items"]:
|
|
assert item["project_name"] == project_name
|
|
|
|
@pytest.mark.integration
|
|
def test_project_uploads_not_found(self, integration_client):
|
|
"""Test non-existent project returns 404."""
|
|
response = integration_client.get("/api/v1/project/nonexistent/uploads")
|
|
assert response.status_code == 404
|