pdftract/crates/pdftract-cli/tests/test_encryption_errors.rs
jedarden b6d01ae85f test(bf-1h5og): add test utility and error handling imports
Add comprehensive test utility and error handling imports to encryption test files:

- Added std::error::Error and std::io for error handling
- Expanded pdftract_core::diagnostics imports to include Diagnostic, DiagnosticsCollector, and ObjRef
- Ensured both test_encryption_errors.rs and test_encryption_unsupported.rs have consistent imports

All imports verified against actual crate structure and compile successfully.

Closes bf-1h5og. Verification: notes/bf-1h5og.md.
2026-07-05 19:01:12 -04:00

366 lines
13 KiB
Rust

//! 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::error::Error;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
// CLI module imports for encryption testing
use pdftract_cli::password;
use pdftract_core::diagnostics::{
DiagCode, DiagInfo, Diagnostic, DiagnosticsCollector, ObjRef, Severity, DIAGNOSTIC_CATALOG,
};
/// 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
}
}