""" Integration tests for download verification API endpoints. These tests verify: - Checksum headers in download responses - Pre-verification mode - Streaming verification mode - HEAD request headers - Verification failure handling """ import pytest import hashlib import base64 import io # ============================================================================= # Test Fixtures # ============================================================================= @pytest.fixture def upload_test_file(integration_client): """ Factory fixture to upload a test file and return its artifact ID. Usage: artifact_id = upload_test_file(project, package, content, version="v1.0") """ def _upload(project_name: str, package_name: str, content: bytes, version: str = None): files = { "file": ("test-file.bin", io.BytesIO(content), "application/octet-stream") } data = {} if version: data["version"] = version response = integration_client.post( f"/api/v1/project/{project_name}/{package_name}/upload", files=files, data=data, ) assert response.status_code == 200, f"Upload failed: {response.text}" return response.json()["artifact_id"] return _upload # ============================================================================= # Integration Tests - Download Headers # ============================================================================= class TestDownloadChecksumHeaders: """Tests for checksum headers in download responses.""" @pytest.mark.integration def test_download_includes_sha256_header( self, integration_client, test_package, upload_test_file ): """Test download response includes X-Checksum-SHA256 header.""" project_name, package_name = test_package content = b"Content for SHA256 header test" # Upload file artifact_id = upload_test_file( project_name, package_name, content, version="sha256-header-test" ) # Download with proxy mode response = integration_client.get( f"/api/v1/project/{project_name}/{package_name}/+/sha256-header-test", params={"mode": "proxy"}, ) assert response.status_code == 200 assert "X-Checksum-SHA256" in response.headers assert response.headers["X-Checksum-SHA256"] == artifact_id @pytest.mark.integration def test_download_includes_etag_header( self, integration_client, test_package, upload_test_file ): """Test download response includes ETag header.""" project_name, package_name = test_package content = b"Content for ETag header test" artifact_id = upload_test_file( project_name, package_name, content, version="etag-test" ) response = integration_client.get( f"/api/v1/project/{project_name}/{package_name}/+/etag-test", params={"mode": "proxy"}, ) assert response.status_code == 200 assert "ETag" in response.headers # ETag should be quoted artifact ID assert response.headers["ETag"] == f'"{artifact_id}"' @pytest.mark.integration def test_download_includes_digest_header( self, integration_client, test_package, upload_test_file ): """Test download response includes RFC 3230 Digest header.""" project_name, package_name = test_package content = b"Content for Digest header test" sha256 = hashlib.sha256(content).hexdigest() upload_test_file(project_name, package_name, content, version="digest-test") response = integration_client.get( f"/api/v1/project/{project_name}/{package_name}/+/digest-test", params={"mode": "proxy"}, ) assert response.status_code == 200 assert "Digest" in response.headers # Verify Digest format: sha-256= digest = response.headers["Digest"] assert digest.startswith("sha-256=") # Verify base64 content matches b64_hash = digest.split("=", 1)[1] decoded = base64.b64decode(b64_hash) assert decoded == bytes.fromhex(sha256) @pytest.mark.integration def test_download_includes_content_length_header( self, integration_client, test_package, upload_test_file ): """Test download response includes X-Content-Length header.""" project_name, package_name = test_package content = b"Content for X-Content-Length test" upload_test_file(project_name, package_name, content, version="content-length-test") response = integration_client.get( f"/api/v1/project/{project_name}/{package_name}/+/content-length-test", params={"mode": "proxy"}, ) assert response.status_code == 200 assert "X-Content-Length" in response.headers assert response.headers["X-Content-Length"] == str(len(content)) @pytest.mark.integration def test_download_includes_verified_header_false( self, integration_client, test_package, upload_test_file ): """Test download without verification has X-Verified: false.""" project_name, package_name = test_package content = b"Content for X-Verified false test" upload_test_file(project_name, package_name, content, version="verified-false-test") response = integration_client.get( f"/api/v1/project/{project_name}/{package_name}/+/verified-false-test", params={"mode": "proxy", "verify": "false"}, ) assert response.status_code == 200 assert "X-Verified" in response.headers assert response.headers["X-Verified"] == "false" # ============================================================================= # Integration Tests - Pre-Verification Mode # ============================================================================= class TestPreVerificationMode: """Tests for pre-verification download mode.""" @pytest.mark.integration def test_pre_verify_success( self, integration_client, test_package, upload_test_file ): """Test pre-verification mode succeeds for valid content.""" project_name, package_name = test_package content = b"Content for pre-verification success test" upload_test_file(project_name, package_name, content, version="pre-verify-success") response = integration_client.get( f"/api/v1/project/{project_name}/{package_name}/+/pre-verify-success", params={"mode": "proxy", "verify": "true", "verify_mode": "pre"}, ) assert response.status_code == 200 assert response.content == content assert "X-Verified" in response.headers assert response.headers["X-Verified"] == "true" @pytest.mark.integration def test_pre_verify_returns_complete_content( self, integration_client, test_package, upload_test_file ): """Test pre-verification returns complete content.""" project_name, package_name = test_package # Use binary content to verify no corruption content = bytes(range(256)) * 10 # 2560 bytes of all byte values upload_test_file(project_name, package_name, content, version="pre-verify-content") response = integration_client.get( f"/api/v1/project/{project_name}/{package_name}/+/pre-verify-content", params={"mode": "proxy", "verify": "true", "verify_mode": "pre"}, ) assert response.status_code == 200 assert response.content == content # ============================================================================= # Integration Tests - Streaming Verification Mode # ============================================================================= class TestStreamingVerificationMode: """Tests for streaming verification download mode.""" @pytest.mark.integration def test_stream_verify_success( self, integration_client, test_package, upload_test_file ): """Test streaming verification mode succeeds for valid content.""" project_name, package_name = test_package content = b"Content for streaming verification success test" upload_test_file( project_name, package_name, content, version="stream-verify-success" ) response = integration_client.get( f"/api/v1/project/{project_name}/{package_name}/+/stream-verify-success", params={"mode": "proxy", "verify": "true", "verify_mode": "stream"}, ) assert response.status_code == 200 assert response.content == content # X-Verified is "pending" for streaming mode (verified after transfer) assert "X-Verified" in response.headers @pytest.mark.integration def test_stream_verify_large_content( self, integration_client, test_package, upload_test_file ): """Test streaming verification with larger content.""" project_name, package_name = test_package # 100KB of content content = b"x" * (100 * 1024) upload_test_file(project_name, package_name, content, version="stream-verify-large") response = integration_client.get( f"/api/v1/project/{project_name}/{package_name}/+/stream-verify-large", params={"mode": "proxy", "verify": "true", "verify_mode": "stream"}, ) assert response.status_code == 200 assert response.content == content # ============================================================================= # Integration Tests - HEAD Request Headers # ============================================================================= class TestHeadRequestHeaders: """Tests for HEAD request checksum headers.""" @pytest.mark.integration def test_head_includes_sha256_header( self, integration_client, test_package, upload_test_file ): """Test HEAD request includes X-Checksum-SHA256 header.""" project_name, package_name = test_package content = b"Content for HEAD SHA256 test" artifact_id = upload_test_file( project_name, package_name, content, version="head-sha256-test" ) response = integration_client.head( f"/api/v1/project/{project_name}/{package_name}/+/head-sha256-test" ) assert response.status_code == 200 assert "X-Checksum-SHA256" in response.headers assert response.headers["X-Checksum-SHA256"] == artifact_id @pytest.mark.integration def test_head_includes_etag( self, integration_client, test_package, upload_test_file ): """Test HEAD request includes ETag header.""" project_name, package_name = test_package content = b"Content for HEAD ETag test" artifact_id = upload_test_file( project_name, package_name, content, version="head-etag-test" ) response = integration_client.head( f"/api/v1/project/{project_name}/{package_name}/+/head-etag-test" ) assert response.status_code == 200 assert "ETag" in response.headers assert response.headers["ETag"] == f'"{artifact_id}"' @pytest.mark.integration def test_head_includes_digest( self, integration_client, test_package, upload_test_file ): """Test HEAD request includes Digest header.""" project_name, package_name = test_package content = b"Content for HEAD Digest test" upload_test_file(project_name, package_name, content, version="head-digest-test") response = integration_client.head( f"/api/v1/project/{project_name}/{package_name}/+/head-digest-test" ) assert response.status_code == 200 assert "Digest" in response.headers assert response.headers["Digest"].startswith("sha-256=") @pytest.mark.integration def test_head_includes_content_length( self, integration_client, test_package, upload_test_file ): """Test HEAD request includes X-Content-Length header.""" project_name, package_name = test_package content = b"Content for HEAD Content-Length test" upload_test_file(project_name, package_name, content, version="head-length-test") response = integration_client.head( f"/api/v1/project/{project_name}/{package_name}/+/head-length-test" ) assert response.status_code == 200 assert "X-Content-Length" in response.headers assert response.headers["X-Content-Length"] == str(len(content)) @pytest.mark.integration def test_head_no_body(self, integration_client, test_package, upload_test_file): """Test HEAD request returns no body.""" project_name, package_name = test_package content = b"Content for HEAD no-body test" upload_test_file(project_name, package_name, content, version="head-no-body-test") response = integration_client.head( f"/api/v1/project/{project_name}/{package_name}/+/head-no-body-test" ) assert response.status_code == 200 assert response.content == b"" # ============================================================================= # Integration Tests - Range Requests # ============================================================================= class TestRangeRequestHeaders: """Tests for range request handling with checksum headers.""" @pytest.mark.integration def test_range_request_includes_checksum_headers( self, integration_client, test_package, upload_test_file ): """Test range request includes checksum headers.""" project_name, package_name = test_package content = b"Content for range request checksum header test" upload_test_file(project_name, package_name, content, version="range-checksum-test") response = integration_client.get( f"/api/v1/project/{project_name}/{package_name}/+/range-checksum-test", headers={"Range": "bytes=0-9"}, params={"mode": "proxy"}, ) assert response.status_code == 206 assert "X-Checksum-SHA256" in response.headers # Checksum is for the FULL file, not the range assert len(response.headers["X-Checksum-SHA256"]) == 64 # ============================================================================= # Integration Tests - Client-Side Verification # ============================================================================= class TestClientSideVerification: """Tests demonstrating client-side verification using headers.""" @pytest.mark.integration def test_client_can_verify_downloaded_content( self, integration_client, test_package, upload_test_file ): """Test client can verify downloaded content using header.""" project_name, package_name = test_package content = b"Content for client-side verification test" upload_test_file(project_name, package_name, content, version="client-verify-test") response = integration_client.get( f"/api/v1/project/{project_name}/{package_name}/+/client-verify-test", params={"mode": "proxy"}, ) assert response.status_code == 200 # Get expected hash from header expected_hash = response.headers["X-Checksum-SHA256"] # Compute actual hash of downloaded content actual_hash = hashlib.sha256(response.content).hexdigest() # Verify match assert actual_hash == expected_hash @pytest.mark.integration def test_client_can_verify_using_digest_header( self, integration_client, test_package, upload_test_file ): """Test client can verify using RFC 3230 Digest header.""" project_name, package_name = test_package content = b"Content for Digest header verification" upload_test_file(project_name, package_name, content, version="digest-verify-test") response = integration_client.get( f"/api/v1/project/{project_name}/{package_name}/+/digest-verify-test", params={"mode": "proxy"}, ) assert response.status_code == 200 # Parse Digest header digest_header = response.headers["Digest"] assert digest_header.startswith("sha-256=") b64_hash = digest_header.split("=", 1)[1] expected_hash_bytes = base64.b64decode(b64_hash) # Compute actual hash of downloaded content actual_hash_bytes = hashlib.sha256(response.content).digest() # Verify match assert actual_hash_bytes == expected_hash_bytes