pdftract/crates/pdftract-cli/tests/test_encryption_errors.rs
jedarden b8a120afd0 test(bf-1or48): verify encryption test modules and fixtures
All acceptance criteria met:
- All 5 test modules present and properly structured
- Fixture validation tests run successfully (not ignored)
- Encrypted PDF tests have #[ignore] with clear reasons
- File compiles successfully with no errors
- 10 encrypted PDF tests ignored, 3 fixture validation tests pass

Verification note: notes/bf-1or48.md
2026-07-06 12:48:29 -04:00

349 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::env;
use std::fs;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::Duration;
// NOTE: This test file uses a subprocess approach (spawning the CLI binary via
// std::process::Command) rather than directly calling internal modules. The CLI
// module imports below are verified to be correct and available for direct module
// testing if needed in the future:
//
// use pdftract_cli::password;
// use pdftract_core::diagnostics::{
// DiagCode, DiagInfo, Diagnostic, DiagnosticsCollector, ObjRef, Severity, DIAGNOSTIC_CATALOG,
// };
//
// Verification results:
// ✅ pdftract_cli::password module exists and exports resolve_password()
// ✅ pdftract_core::diagnostics module exists and exports all imported types:
// - DiagCode, DiagInfo, Diagnostic, DiagnosticsCollector, ObjRef, Severity, DIAGNOSTIC_CATALOG
// ✅ All import paths match actual crate structure
// ✅ File compiles successfully with imports uncommented (verified via cargo build)
//
// These imports remain commented because the current test implementation uses
// subprocess calls and does not require direct module access. Uncomment only if
// implementing direct module testing.
/// Get the workspace root directory
fn workspace_root() -> PathBuf {
let manifest_dir = 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(Stdio::piped())
.stdout(Stdio::piped())
.stderr(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
}
}