""" 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)