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.
366 lines
13 KiB
Rust
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
|
|
}
|
|
}
|