pdftract/crates/pdftract-core/tests/remote_tls_tests.rs
2026-05-29 08:25:23 -04:00

201 lines
6.9 KiB
Rust

//! TLS failure tests for remote source adapter (Phase 1.8).
//!
//! These tests verify that TLS handshake failures produce clear error messages
//! and the correct exit code (6) for certificate failures.
#![cfg(feature = "remote")]
use std::io;
use pdftract_core::source::{open_remote, RemoteOpts};
/// Test 1: TLS handshake with self-signed cert (via badssl.com).
///
/// Note: ureq's rustls backend rejects self-signed certs by default.
/// This test verifies that we get a clear TLS error message.
#[tokio::test]
async fn test_tls_self_signed_cert_rejected() {
// Use badssl.com's self-signed cert endpoint
let url = "https://self-signed.badssl.com/";
let opts = RemoteOpts::new();
// TLS handshake should fail due to self-signed cert
let result = open_remote(url, &opts, None);
// Should fail with a TLS-related error
assert!(result.is_err(), "Self-signed cert should be rejected");
if let Err(e) = result {
// Should be PermissionDenied (TLS failure) or a transport error
let kind = e.kind();
assert!(
kind == io::ErrorKind::PermissionDenied || kind == io::ErrorKind::Other,
"TLS failure should return PermissionDenied or Other, got: {:?}",
kind
);
// Error message should mention TLS or certificate
let msg = e.to_string().to_lowercase();
assert!(
msg.contains("tls") || msg.contains("certificate") || msg.contains("handshake") || msg.contains("verify"),
"Error message should mention TLS/certificate/handshake/verify, got: {}",
e
);
}
}
/// Test 2: TLS handshake with expired cert (via badssl.com).
#[tokio::test]
async fn test_tls_expired_cert_rejected() {
// Use badssl.com's expired cert endpoint
let url = "https://expired.badssl.com/";
let opts = RemoteOpts::new();
// TLS handshake should fail due to expired cert
let result = open_remote(url, &opts, None);
assert!(result.is_err(), "Expired cert should be rejected");
if let Err(e) = result {
let msg = e.to_string().to_lowercase();
assert!(
msg.contains("tls") || msg.contains("certificate") || msg.contains("expired") || msg.contains("valid"),
"Error message should mention TLS/certificate/expired/valid, got: {}",
e
);
}
}
/// Test 3: TLS handshake with wrong host cert (via badssl.com).
#[tokio::test]
async fn test_tls_wrong_host_rejected() {
// Use badssl.com's wrong host endpoint
let url = "https://wrong.host.badssl.com/";
let opts = RemoteOpts::new();
let result = open_remote(url, &opts, None);
// Should fail due to hostname mismatch
assert!(result.is_err());
if let Err(e) = result {
let msg = e.to_string().to_lowercase();
// The error should be related to TLS validation
assert!(
msg.contains("tls") || msg.contains("certificate") || msg.contains("host") || msg.contains("verify"),
"Error should mention TLS/certificate/host/verify, got: {}",
e
);
}
}
/// Test 4: Verify TLS error produces exit code 6 (via error kind).
#[tokio::test]
async fn test_tls_error_exit_code() {
// Use a known HTTPS endpoint with invalid cert
let url = "https://expired.badssl.com/";
let opts = RemoteOpts::new();
let result = open_remote(url, &opts, None);
if let Err(e) = result {
// TLS errors should produce PermissionDenied kind
// The CLI maps PermissionDenied to exit code 6
assert_eq!(e.kind(), io::ErrorKind::PermissionDenied,
"TLS failure should produce PermissionDenied error kind for exit code 6");
}
}
/// Test 5: Verify valid HTTPS works (via badssl.com).
#[tokio::test]
#[ignore = "Requires full internet access - may be flaky in CI"]
async fn test_tls_valid_cert_works() {
// Use badssl.com's valid cert endpoint
let url = "https://sha256.badssl.com/";
let opts = RemoteOpts::new();
let result = open_remote(url, &opts, None);
// This should work or at least get past TLS validation
// (might fail due to not being a PDF, but TLS should succeed)
if let Err(e) = result {
let msg = e.to_string().to_lowercase();
// Should NOT be a TLS/certificate error
assert!(!msg.contains("tls") && !msg.contains("certificate") && !msg.contains("handshake"),
"Valid HTTPS should not trigger TLS errors, got: {}", e);
}
}
/// Test 6: TLS connection timeout.
#[tokio::test]
async fn test_tls_connection_timeout() {
// Use a non-routable IP to trigger timeout
let url = "https://192.0.2.1/test.pdf"; // TEST-NET-1, never routable
let opts = RemoteOpts::new();
let result = open_remote(url, &opts, None);
assert!(result.is_err());
if let Err(e) = result {
// Should be a timeout or connection error
let kind = e.kind();
assert!(
kind == io::ErrorKind::TimedOut || kind == io::ErrorKind::Interrupted,
"Connection timeout should produce TimedOut or Interrupted, got: {:?}",
kind
);
}
}
/// Test 7: Verify INV-8 - no panic on TLS errors.
#[tokio::test]
async fn test_inv8_no_panic_on_tls_errors() {
let result = std::panic::catch_unwind(|| {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let opts = RemoteOpts::new();
let _ = open_remote("https://expired.badssl.com/", &opts, None);
});
});
assert!(result.is_ok(), "Should not panic on TLS errors");
}
/// Test 8: Verify that HTTP URLs don't trigger TLS validation.
#[tokio::test]
#[cfg(feature = "remote")]
async fn test_http_no_tls_validation() {
use wiremock::{MockServer, Mock, ResponseTemplate, matchers::{method, path}};
let mock_server = MockServer::start().await;
Mock::given(method("HEAD"))
.and(path("/test.pdf"))
.respond_with(
ResponseTemplate::new(200)
.insert_header("Content-Length", "1000")
.insert_header("Accept-Ranges", "bytes")
.insert_header("Content-Type", "application/pdf")
.set_body_bytes("")
)
.mount(&mock_server)
.await;
// Get the HTTP URL from wiremock
let url = format!("{}/test.pdf", mock_server.uri());
// Verify it's HTTP, not HTTPS
assert!(url.starts_with("http://"), "Wiremock should provide HTTP URLs");
let opts = RemoteOpts::new();
let result = open_remote(&url, &opts, None);
// HTTP should work (no TLS validation needed)
// Note: This test verifies that we correctly distinguish HTTP vs HTTPS URLs
if let Err(e) = result {
// If it fails, it shouldn't be a TLS error
let msg = e.to_string().to_lowercase();
assert!(!msg.contains("tls") && !msg.contains("certificate") && !msg.contains("handshake"),
"HTTP URLs should not trigger TLS validation errors, got: {}", e);
}
}