//! Comprehensive encryption error tests for pdftract CLI //! //! This module tests encryption-related error cases and verifies that: //! 1. Unsupported encryption handlers emit ENCRYPTION_UNSUPPORTED and exit code 3 //! 2. Supported encrypted PDFs can be decrypted with correct passwords //! 3. Wrong/missing passwords emit appropriate errors //! 4. Various encryption algorithms (RC4, AES-128, AES-256) are handled correctly //! //! Test fixtures: //! - tests/fixtures/encrypted/EC-04-rc4-encrypted.pdf (RC4 encryption) //! - tests/fixtures/encrypted/EC-05-aes128-encrypted.pdf (AES-128 encryption) //! - tests/fixtures/encrypted/EC-06-aes256-encrypted.pdf (AES-256 encryption) //! - tests/fixtures/encrypted/EC-empty-password.pdf (empty password) //! - tests/fixtures/encrypted/livecycle.pdf (unsupported Adobe.APS handler) //! //! References: //! - Plan line 258, Failure Mode Taxonomy: ENCRYPTION_UNSUPPORTED //! - Plan line 732: Encryption-unsupported diagnostic and CLI exit code 3 //! - Plan line 1132: RC4 and AES-128/256 decryption implementation //! - Plan line 1149: Encrypted file with unknown handler error handling use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; /// Get the workspace root directory fn workspace_root() -> PathBuf { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); let path = PathBuf::from(manifest_dir); // We're in crates/pdftract-cli, so go up two levels to reach workspace root path.parent().unwrap().parent().unwrap().to_path_buf() } /// Path to encrypted fixtures directory fn encrypted_fixture_dir() -> PathBuf { workspace_root().join("tests/fixtures/encrypted") } /// Path to a specific encrypted fixture fn encrypted_fixture(name: &str) -> PathBuf { encrypted_fixture_dir().join(name) } /// Path to pdftract CLI binary fn pdftract_bin() -> PathBuf { let mut path = workspace_root(); path.push("target/debug/pdftract"); path } /// Exit code for encryption errors (per spec) const ENCRYPTION_EXIT_CODE: i32 = 3; /// Expected encrypted fixture files const ENCRYPTED_FIXTURES: &[&str] = &[ "EC-04-rc4-encrypted.pdf", "EC-05-aes128-encrypted.pdf", "EC-06-aes256-encrypted.pdf", "EC-empty-password.pdf", "livecycle.pdf", ]; // ============================================================================ // Module: Unsupported Encryption Handlers // ============================================================================ mod unsupported_handlers { use super::*; /// Test that livecycle.pdf (unsupported Adobe.APS encryption handler) /// triggers ENCRYPTION_UNSUPPORTED and exits with code 3 #[test] #[ignore = "Requires ENCRYPTION_UNSUPPORTED exit code implementation"] fn test_livecycle_pdf_emits_encryption_unsupported() { let fixture = encrypted_fixture("livecycle.pdf"); assert!( fixture.exists(), "Fixture not found: {}", fixture.display() ); let output = Command::new(pdftract_bin()) .args(["extract", fixture.to_str().unwrap()]) .output() .expect("Failed to run pdftract extract on livecycle.pdf"); // Exit code 3 for encryption errors (per spec) assert_eq!( output.status.code(), Some(ENCRYPTION_EXIT_CODE), "Expected exit code {} for ENCRYPTION_UNSUPPORTED, got {:?}", ENCRYPTION_EXIT_CODE, output.status.code() ); // stderr should contain the error message let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("Unsupported encryption") || stderr.contains("ENCRYPTION_UNSUPPORTED"), "Expected stderr to contain 'Unsupported encryption' or 'ENCRYPTION_UNSUPPORTED', got: {}", stderr ); // stdout should be empty or minimal (no extraction output) let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.is_empty() || !stdout.contains("\"pages\""), "Expected no extraction output in stdout, got: {}", stdout ); } /// Test that even with a password, livecycle.pdf still fails because /// the Adobe.APS handler is unsupported (not a password issue) #[test] #[ignore = "Requires --password-stdin implementation or INSECURE_CLI_PASSWORD handling"] fn test_livecycle_pdf_with_password_also_fails() { let fixture = encrypted_fixture("livecycle.pdf"); assert!( fixture.exists(), "Fixture not found: {}", fixture.display() ); let output = Command::new(pdftract_bin()) .args(["extract", fixture.to_str().unwrap()]) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .output() .expect("Failed to run pdftract extract with password stdin"); // Should still exit with code 3 (unsupported algorithm, not wrong password) assert_eq!( output.status.code(), Some(ENCRYPTION_EXIT_CODE), "Expected exit code {} for unsupported algorithm even with password, got {:?}", ENCRYPTION_EXIT_CODE, output.status.code() ); } } // ============================================================================ // Module: Supported Encryption with Correct Passwords // ============================================================================ mod supported_encryption { use super::*; /// Test that RC4-encrypted PDF can be decrypted with correct password #[test] #[ignore = "Requires password-based decryption implementation"] fn test_rc4_encrypted_with_correct_password() { let fixture = encrypted_fixture("EC-04-rc4-encrypted.pdf"); assert!( fixture.exists(), "Fixture not found: {}", fixture.display() ); // TODO: Implement once password CLI flag is available // Expected: successful extraction with correct password } /// Test that AES-128-encrypted PDF can be decrypted with correct password #[test] #[ignore = "Requires password-based decryption implementation"] fn test_aes128_encrypted_with_correct_password() { let fixture = encrypted_fixture("EC-05-aes128-encrypted.pdf"); assert!( fixture.exists(), "Fixture not found: {}", fixture.display() ); // TODO: Implement once password CLI flag is available // Expected: successful extraction with correct password } /// Test that AES-256-encrypted PDF can be decrypted with correct password #[test] #[ignore = "Requires password-based decryption implementation"] fn test_aes256_encrypted_with_correct_password() { let fixture = encrypted_fixture("EC-06-aes256-encrypted.pdf"); assert!( fixture.exists(), "Fixture not found: {}", fixture.display() ); // TODO: Implement once password CLI flag is available // Expected: successful extraction with correct password } /// Test that empty-password PDF can be opened (no password required) #[test] #[ignore = "Requires empty password handling implementation"] fn test_empty_password_pdf_opens() { let fixture = encrypted_fixture("EC-empty-password.pdf"); assert!( fixture.exists(), "Fixture not found: {}", fixture.display() ); // TODO: Implement once empty password auto-detection is available // Expected: successful extraction without password } } // ============================================================================ // Module: Wrong/Missing Password Handling // ============================================================================ mod password_errors { use super::*; /// Test that wrong password emits appropriate error #[test] #[ignore = "Requires password-based decryption implementation"] fn test_wrong_password_emits_error() { let fixture = encrypted_fixture("EC-04-rc4-encrypted.pdf"); assert!( fixture.exists(), "Fixture not found: {}", fixture.display() ); // TODO: Implement once password CLI flag is available // Expected: exit code 3 with appropriate error message } /// Test that missing required password emits appropriate error #[test] #[ignore = "Requires password-based decryption implementation"] fn test_missing_required_password_emits_error() { let fixture = encrypted_fixture("EC-05-aes128-encrypted.pdf"); assert!( fixture.exists(), "Fixture not found: {}", fixture.display() ); // TODO: Implement once password requirement detection is available // Expected: exit code 3 with appropriate error message } } // ============================================================================ // Module: Fixture Structure Validation // ============================================================================ mod fixture_validation { use super::*; /// Verify all encrypted fixture files exist #[test] fn test_encrypted_fixtures_exist() { let fixture_dir = encrypted_fixture_dir(); assert!( fixture_dir.exists(), "Encrypted fixtures directory not found: {}", fixture_dir.display() ); for fixture_name in ENCRYPTED_FIXTURES { let path = encrypted_fixture(fixture_name); assert!( path.exists(), "Encrypted fixture not found: {}", path.display() ); } } /// Verify fixture files are valid PDFs (basic structure check) #[test] fn test_encrypted_fixtures_are_valid_pdfs() { for fixture_name in ENCRYPTED_FIXTURES { let path = encrypted_fixture(fixture_name); let content = fs::read(&path).expect(&format!( "Failed to read fixture: {}", path.display() )); // Check for PDF magic number (%PDF-) assert!( content.starts_with(b"%PDF-"), "Fixture {} is not a valid PDF (missing %PDF- header)", fixture_name ); // Check for EOF marker let content_str = String::from_utf8_lossy(&content); assert!( content_str.contains("%%EOF"), "Fixture {} is not a valid PDF (missing %%EOF marker)", fixture_name ); } } /// Verify expected output files exist for supported encryption tests #[test] fn test_expected_outputs_exist() { let fixture_dir = encrypted_fixture_dir(); // Check for expected output files for supported encryption types let expected_outputs = &[ "EC-04-rc4-encrypted.expected.json", "EC-05-aes128-encrypted.expected.json", ]; for expected_name in expected_outputs { let path = fixture_dir.join(expected_name); assert!( path.exists(), "Expected output file not found: {}", path.display() ); } } } // ============================================================================ // Module: Integration Tests (Placeholder for Future Implementation) // ============================================================================ #[cfg(test)] mod integration_tests { use super::*; /// Integration test: Complete extraction workflow for supported encrypted PDFs #[test] #[ignore = "Requires complete password-based decryption implementation"] fn test_encrypted_pdf_extraction_workflow() { // This will test the complete workflow: // 1. Detect encryption // 2. Prompt for password / accept --password flag // 3. Decrypt PDF // 4. Extract content // 5. Verify output matches expected.json } /// Integration test: Error recovery workflow for unsupported encryption #[test] #[ignore = "Requires error recovery implementation"] fn test_unsupported_encryption_error_recovery() { // This will test error recovery: // 1. Detect unsupported encryption // 2. Emit ENCRYPTION_UNSUPPORTED diagnostic // 3. Exit with code 3 // 4. Provide helpful error message } }