""" Integration tests for error handling in upload and download operations. Tests cover: - Timeout handling - Invalid request handling - Resource cleanup on failures - Graceful error responses """ import pytest import io import time from tests.factories import ( compute_sha256, upload_test_file, generate_content_with_hash, ) class TestUploadErrorHandling: """Tests for upload error handling.""" @pytest.mark.integration def test_upload_to_nonexistent_project_returns_404( self, integration_client, unique_test_id ): """Test upload to nonexistent project returns 404.""" content = b"test content for nonexistent project" files = {"file": ("test.bin", io.BytesIO(content), "application/octet-stream")} response = integration_client.post( f"/api/v1/project/nonexistent-project-{unique_test_id}/nonexistent-pkg/upload", files=files, ) assert response.status_code == 404 @pytest.mark.integration def test_upload_to_nonexistent_package_returns_404( self, integration_client, test_project, unique_test_id ): """Test upload to nonexistent package returns 404.""" content = b"test content for nonexistent package" files = {"file": ("test.bin", io.BytesIO(content), "application/octet-stream")} response = integration_client.post( f"/api/v1/project/{test_project}/nonexistent-package-{unique_test_id}/upload", files=files, ) assert response.status_code == 404 @pytest.mark.integration def test_upload_empty_file_rejected(self, integration_client, test_package): """Test empty file upload is rejected.""" project, package = test_package files = {"file": ("empty.bin", io.BytesIO(b""), "application/octet-stream")} response = integration_client.post( f"/api/v1/project/{project}/{package}/upload", files=files, ) assert response.status_code in [400, 422] @pytest.mark.integration def test_upload_missing_file_returns_422(self, integration_client, test_package): """Test upload without file field returns 422.""" project, package = test_package response = integration_client.post( f"/api/v1/project/{project}/{package}/upload", data={"version": "no-file-provided"}, ) assert response.status_code == 422 @pytest.mark.integration def test_upload_invalid_checksum_format_returns_400( self, integration_client, test_package ): """Test upload with invalid checksum format returns 400.""" project, package = test_package content = b"checksum format test" files = {"file": ("test.bin", io.BytesIO(content), "application/octet-stream")} response = integration_client.post( f"/api/v1/project/{project}/{package}/upload", files=files, headers={"X-Checksum-SHA256": "invalid-hash-format"}, ) assert response.status_code == 400 @pytest.mark.integration def test_upload_checksum_mismatch_returns_422( self, integration_client, test_package ): """Test upload with mismatched checksum returns 422.""" project, package = test_package content = b"checksum mismatch test" wrong_hash = "0" * 64 # Valid format but wrong hash files = {"file": ("test.bin", io.BytesIO(content), "application/octet-stream")} response = integration_client.post( f"/api/v1/project/{project}/{package}/upload", files=files, headers={"X-Checksum-SHA256": wrong_hash}, ) assert response.status_code == 422 @pytest.mark.integration def test_upload_with_correct_checksum_succeeds( self, integration_client, test_package ): """Test upload with correct checksum succeeds.""" project, package = test_package content = b"correct checksum test" correct_hash = compute_sha256(content) files = {"file": ("test.bin", io.BytesIO(content), "application/octet-stream")} response = integration_client.post( f"/api/v1/project/{project}/{package}/upload", files=files, headers={"X-Checksum-SHA256": correct_hash}, ) assert response.status_code == 200 assert response.json()["artifact_id"] == correct_hash class TestDownloadErrorHandling: """Tests for download error handling.""" @pytest.mark.integration def test_download_nonexistent_tag_returns_404( self, integration_client, test_package ): """Test download of nonexistent tag returns 404.""" project, package = test_package response = integration_client.get( f"/api/v1/project/{project}/{package}/+/nonexistent-tag-xyz" ) assert response.status_code == 404 @pytest.mark.integration def test_download_nonexistent_artifact_returns_404( self, integration_client, test_package ): """Test download of nonexistent artifact ID returns 404.""" project, package = test_package fake_hash = "a" * 64 response = integration_client.get( f"/api/v1/project/{project}/{package}/+/artifact:{fake_hash}" ) assert response.status_code == 404 @pytest.mark.integration def test_download_invalid_artifact_id_format( self, integration_client, test_package ): """Test download with invalid artifact ID format.""" project, package = test_package # Too short response = integration_client.get( f"/api/v1/project/{project}/{package}/+/artifact:abc123" ) assert response.status_code == 404 @pytest.mark.integration def test_download_from_nonexistent_project_returns_404( self, integration_client, unique_test_id ): """Test download from nonexistent project returns 404.""" response = integration_client.get( f"/api/v1/project/nonexistent-{unique_test_id}/pkg/+/tag" ) assert response.status_code == 404 @pytest.mark.integration def test_download_from_nonexistent_package_returns_404( self, integration_client, test_project, unique_test_id ): """Test download from nonexistent package returns 404.""" response = integration_client.get( f"/api/v1/project/{test_project}/nonexistent-{unique_test_id}/+/tag" ) assert response.status_code == 404 class TestTimeoutBehavior: """Tests for timeout behavior (integration level).""" @pytest.mark.integration @pytest.mark.slow def test_large_upload_completes_within_reasonable_time( self, integration_client, test_package, sized_content ): """Test that a 10MB upload completes within reasonable time.""" project, package = test_package content, expected_hash = sized_content(10 * 1024 * 1024, seed=999) # 10MB start_time = time.time() result = upload_test_file( integration_client, project, package, content, version="timeout-test" ) elapsed = time.time() - start_time assert result["artifact_id"] == expected_hash # Should complete within 60 seconds for 10MB on local docker assert elapsed < 60, f"Upload took too long: {elapsed:.2f}s" @pytest.mark.integration @pytest.mark.slow def test_large_download_completes_within_reasonable_time( self, integration_client, test_package, sized_content ): """Test that a 10MB download completes within reasonable time.""" project, package = test_package content, expected_hash = sized_content(10 * 1024 * 1024, seed=998) # 10MB # First upload upload_test_file( integration_client, project, package, content, version="download-timeout-test" ) # Then download and time it start_time = time.time() response = integration_client.get( f"/api/v1/project/{project}/{package}/+/download-timeout-test", params={"mode": "proxy"}, ) elapsed = time.time() - start_time assert response.status_code == 200 assert len(response.content) == len(content) # Should complete within 60 seconds for 10MB on local docker assert elapsed < 60, f"Download took too long: {elapsed:.2f}s" class TestResourceCleanup: """Tests for proper resource cleanup on failures. Note: More comprehensive cleanup tests are in test_upload_download_api.py (TestUploadFailureCleanup class) including S3 object cleanup verification. """ @pytest.mark.integration def test_checksum_mismatch_no_orphaned_artifact( self, integration_client, test_package, unique_test_id ): """Test checksum mismatch doesn't leave orphaned artifact.""" project, package = test_package # Use unique content to ensure artifact doesn't exist from prior tests content = f"checksum mismatch orphan test {unique_test_id}".encode() wrong_hash = "0" * 64 actual_hash = compute_sha256(content) # Verify artifact doesn't exist before test pre_check = integration_client.get(f"/api/v1/artifact/{actual_hash}") assert pre_check.status_code == 404, "Artifact should not exist before test" files = {"file": ("test.bin", io.BytesIO(content), "application/octet-stream")} response = integration_client.post( f"/api/v1/project/{project}/{package}/upload", files=files, headers={"X-Checksum-SHA256": wrong_hash}, ) assert response.status_code == 422 # Verify no artifact was created with either hash response1 = integration_client.get(f"/api/v1/artifact/{wrong_hash}") response2 = integration_client.get(f"/api/v1/artifact/{actual_hash}") assert response1.status_code == 404 assert response2.status_code == 404 class TestGracefulErrorResponses: """Tests for graceful and informative error responses.""" @pytest.mark.integration def test_404_response_has_detail_message( self, integration_client, test_package ): """Test 404 responses include a detail message.""" project, package = test_package response = integration_client.get( f"/api/v1/project/{project}/{package}/+/nonexistent-tag" ) assert response.status_code == 404 data = response.json() assert "detail" in data assert len(data["detail"]) > 0 @pytest.mark.integration def test_422_response_has_detail_message(self, integration_client, test_package): """Test 422 responses include a detail message.""" project, package = test_package # Upload with mismatched checksum content = b"detail message test" wrong_hash = "0" * 64 files = {"file": ("test.bin", io.BytesIO(content), "application/octet-stream")} response = integration_client.post( f"/api/v1/project/{project}/{package}/upload", files=files, headers={"X-Checksum-SHA256": wrong_hash}, ) assert response.status_code == 422 data = response.json() assert "detail" in data @pytest.mark.integration def test_error_response_is_json(self, integration_client, unique_test_id): """Test error responses are valid JSON.""" response = integration_client.get( f"/api/v1/project/nonexistent-{unique_test_id}/pkg/+/tag" ) assert response.status_code == 404 # Should not raise exception - valid JSON data = response.json() assert isinstance(data, dict)