169 lines
6.7 KiB
Python
169 lines
6.7 KiB
Python
"""
|
|
Integration tests for garbage collection functionality.
|
|
|
|
Tests cover:
|
|
- Listing orphaned artifacts (ref_count=0)
|
|
- Garbage collection in dry-run mode
|
|
- Garbage collection actual deletion
|
|
- Verifying artifacts with refs are not deleted
|
|
"""
|
|
|
|
import pytest
|
|
from tests.conftest import (
|
|
compute_sha256,
|
|
upload_test_file,
|
|
)
|
|
|
|
|
|
class TestOrphanedArtifactsEndpoint:
|
|
"""Tests for GET /api/v1/admin/orphaned-artifacts endpoint."""
|
|
|
|
@pytest.mark.integration
|
|
def test_list_orphaned_artifacts_returns_list(self, integration_client):
|
|
"""Test orphaned artifacts endpoint returns a list."""
|
|
response = integration_client.get("/api/v1/admin/orphaned-artifacts")
|
|
assert response.status_code == 200
|
|
assert isinstance(response.json(), list)
|
|
|
|
@pytest.mark.integration
|
|
def test_orphaned_artifact_has_required_fields(self, integration_client):
|
|
"""Test orphaned artifact response has required fields."""
|
|
response = integration_client.get("/api/v1/admin/orphaned-artifacts?limit=1")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
if len(data) > 0:
|
|
artifact = data[0]
|
|
assert "id" in artifact
|
|
assert "size" in artifact
|
|
assert "created_at" in artifact
|
|
assert "created_by" in artifact
|
|
assert "original_name" in artifact
|
|
|
|
@pytest.mark.integration
|
|
def test_orphaned_artifacts_respects_limit(self, integration_client):
|
|
"""Test orphaned artifacts endpoint respects limit parameter."""
|
|
response = integration_client.get("/api/v1/admin/orphaned-artifacts?limit=5")
|
|
assert response.status_code == 200
|
|
assert len(response.json()) <= 5
|
|
|
|
@pytest.mark.integration
|
|
def test_artifact_becomes_orphaned_when_tag_deleted(
|
|
self, integration_client, test_package, unique_test_id
|
|
):
|
|
"""Test artifact appears in orphaned list after tag is deleted."""
|
|
project, package = test_package
|
|
content = f"orphan test {unique_test_id}".encode()
|
|
expected_hash = compute_sha256(content)
|
|
|
|
# Upload with tag
|
|
upload_test_file(integration_client, project, package, content, tag="temp-tag")
|
|
|
|
# Verify not in orphaned list (has ref_count=1)
|
|
response = integration_client.get("/api/v1/admin/orphaned-artifacts?limit=1000")
|
|
orphaned_ids = [a["id"] for a in response.json()]
|
|
assert expected_hash not in orphaned_ids
|
|
|
|
# Delete the tag
|
|
integration_client.delete(f"/api/v1/project/{project}/{package}/tags/temp-tag")
|
|
|
|
# Verify now in orphaned list (ref_count=0)
|
|
response = integration_client.get("/api/v1/admin/orphaned-artifacts?limit=1000")
|
|
orphaned_ids = [a["id"] for a in response.json()]
|
|
assert expected_hash in orphaned_ids
|
|
|
|
|
|
class TestGarbageCollectionEndpoint:
|
|
"""Tests for POST /api/v1/admin/garbage-collect endpoint."""
|
|
|
|
@pytest.mark.integration
|
|
def test_garbage_collect_dry_run_returns_response(self, integration_client):
|
|
"""Test garbage collection dry run returns valid response."""
|
|
response = integration_client.post("/api/v1/admin/garbage-collect?dry_run=true")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert "artifacts_deleted" in data
|
|
assert "bytes_freed" in data
|
|
assert "artifact_ids" in data
|
|
assert "dry_run" in data
|
|
assert data["dry_run"] is True
|
|
|
|
@pytest.mark.integration
|
|
def test_garbage_collect_dry_run_doesnt_delete(
|
|
self, integration_client, test_package, unique_test_id
|
|
):
|
|
"""Test garbage collection dry run doesn't actually delete artifacts."""
|
|
project, package = test_package
|
|
content = f"dry run test {unique_test_id}".encode()
|
|
expected_hash = compute_sha256(content)
|
|
|
|
# Upload and delete tag to create orphan
|
|
upload_test_file(integration_client, project, package, content, tag="dry-run")
|
|
integration_client.delete(f"/api/v1/project/{project}/{package}/tags/dry-run")
|
|
|
|
# Verify artifact exists
|
|
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
|
assert response.status_code == 200
|
|
|
|
# Run garbage collection in dry-run mode
|
|
gc_response = integration_client.post(
|
|
"/api/v1/admin/garbage-collect?dry_run=true&limit=1000"
|
|
)
|
|
assert gc_response.status_code == 200
|
|
assert expected_hash in gc_response.json()["artifact_ids"]
|
|
|
|
# Verify artifact STILL exists (dry run didn't delete)
|
|
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
|
assert response.status_code == 200
|
|
|
|
@pytest.mark.integration
|
|
def test_garbage_collect_preserves_referenced_artifacts(
|
|
self, integration_client, test_package, unique_test_id
|
|
):
|
|
"""Test garbage collection doesn't delete artifacts with ref_count > 0."""
|
|
project, package = test_package
|
|
content = f"preserve test {unique_test_id}".encode()
|
|
expected_hash = compute_sha256(content)
|
|
|
|
# Upload with tag (ref_count=1)
|
|
upload_test_file(integration_client, project, package, content, tag="keep-this")
|
|
|
|
# Verify artifact exists with ref_count=1
|
|
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
|
assert response.status_code == 200
|
|
assert response.json()["ref_count"] == 1
|
|
|
|
# Run garbage collection (dry_run to not affect other tests)
|
|
gc_response = integration_client.post(
|
|
"/api/v1/admin/garbage-collect?dry_run=true&limit=1000"
|
|
)
|
|
assert gc_response.status_code == 200
|
|
|
|
# Verify artifact was NOT in delete list (has ref_count > 0)
|
|
assert expected_hash not in gc_response.json()["artifact_ids"]
|
|
|
|
# Verify artifact still exists
|
|
response = integration_client.get(f"/api/v1/artifact/{expected_hash}")
|
|
assert response.status_code == 200
|
|
assert response.json()["ref_count"] == 1
|
|
|
|
@pytest.mark.integration
|
|
def test_garbage_collect_respects_limit(self, integration_client):
|
|
"""Test garbage collection respects limit parameter."""
|
|
response = integration_client.post(
|
|
"/api/v1/admin/garbage-collect?dry_run=true&limit=5"
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["artifacts_deleted"] <= 5
|
|
|
|
@pytest.mark.integration
|
|
def test_garbage_collect_returns_bytes_freed(self, integration_client):
|
|
"""Test garbage collection returns accurate bytes_freed."""
|
|
response = integration_client.post("/api/v1/admin/garbage-collect?dry_run=true")
|
|
assert response.status_code == 200
|
|
|
|
data = response.json()
|
|
assert data["bytes_freed"] >= 0
|
|
assert isinstance(data["bytes_freed"], int)
|