201 lines
6.9 KiB
Rust
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);
|
|
}
|
|
}
|