| 1 | use super::*; |
| 2 | use crate::tools::spec::ToolContext; |
| 3 | use std::path::PathBuf; |
| 4 | |
| 5 | struct ArtifactRootRestore(Option<PathBuf>); |
| 6 | |
| 7 | impl Drop for ArtifactRootRestore { |
| 8 | fn drop(&mut self) { |
| 9 | crate::artifacts::set_test_artifact_sessions_root(self.0.take()); |
| 10 | } |
| 11 | } |
| 12 | |
| 13 | fn runtime() -> tokio::runtime::Runtime { |
| 14 | tokio::runtime::Builder::new_current_thread() |
| 15 | .enable_all() |
| 16 | .build() |
| 17 | .expect("test runtime") |
| 18 | } |
| 19 | |
| 20 | #[test] |
| 21 | fn raw_pdf_production_path_preserves_exact_bytes_without_extractor() { |
| 22 | let _lock = crate::artifacts::TEST_ARTIFACT_SESSIONS_GUARD |
| 23 | .lock() |
| 24 | .unwrap_or_else(|error| error.into_inner()); |
| 25 | let temporary = tempfile::tempdir().expect("artifact root"); |
| 26 | let prior = |
| 27 | crate::artifacts::set_test_artifact_sessions_root(Some(temporary.path().join("sessions"))); |
| 28 | let _restore = ArtifactRootRestore(prior); |
| 29 | let bytes = b"%PDF-1.7\nraw fixture that is intentionally not parseable\n%%EOF"; |
| 30 | let missing = temporary.path().join("definitely-not-pdftotext"); |
| 31 | let document = runtime() |
| 32 | .block_on(extract_fetched_document( |
| 33 | Format::Raw, |
| 34 | "https://example.com/raw.pdf", |
| 35 | "application/pdf", |
| 36 | bytes, |
| 37 | true, |
| 38 | None, |
| 39 | PdfTextCommand::test(missing.as_os_str(), Duration::from_millis(50), None), |
| 40 | )) |
| 41 | .expect("signed raw PDF must bypass the missing extractor"); |
| 42 | let (content, artifact) = render_extracted( |
| 43 | "https://example.com/raw.pdf", |
| 44 | "application/pdf", |
| 45 | Format::Raw, |
| 46 | document, |
| 47 | bytes, |
| 48 | &ToolContext::new("."), |
| 49 | ) |
| 50 | .expect("raw PDF preservation must not require pdftotext"); |
| 51 | let artifact = artifact.expect("raw PDF artifact"); |
| 52 | assert!(content.contains("PDF response saved"), "{content}"); |
| 53 | assert_eq!(std::fs::read(artifact.absolute_path).unwrap(), bytes); |
| 54 | } |
| 55 | |
| 56 | #[test] |
| 57 | fn raw_pdf_spoofs_and_contradictory_media_mime_create_no_artifact() { |
| 58 | let _lock = crate::artifacts::TEST_ARTIFACT_SESSIONS_GUARD |
| 59 | .lock() |
| 60 | .unwrap_or_else(|error| error.into_inner()); |
| 61 | let temporary = tempfile::tempdir().expect("artifact root"); |
| 62 | let sessions = temporary.path().join("sessions"); |
| 63 | let prior = crate::artifacts::set_test_artifact_sessions_root(Some(sessions.clone())); |
| 64 | let _restore = ArtifactRootRestore(prior); |
| 65 | let missing = temporary.path().join("definitely-not-pdftotext"); |
| 66 | let request = PdfTextCommand::test(missing.as_os_str(), Duration::from_millis(50), None); |
| 67 | let runtime = runtime(); |
| 68 | |
| 69 | for (url, content_type, bytes, expected) in [ |
| 70 | ( |
| 71 | "https://example.com/download", |
| 72 | "application/pdf", |
| 73 | b"plain text pretending to be a PDF".as_slice(), |
| 74 | "PDF signature", |
| 75 | ), |
| 76 | ( |
| 77 | "https://example.com/spoof.pdf", |
| 78 | "text/plain", |
| 79 | b"plain text pretending to be a PDF".as_slice(), |
| 80 | "PDF signature", |
| 81 | ), |
| 82 | ( |
| 83 | "https://example.com/signed", |
| 84 | "image/png", |
| 85 | b"%PDF-1.7\n%%EOF".as_slice(), |
| 86 | "did not match its PDF bytes", |
| 87 | ), |
| 88 | ] { |
| 89 | let error = runtime |
| 90 | .block_on(extract_fetched_document( |
| 91 | Format::Raw, |
| 92 | url, |
| 93 | content_type, |
| 94 | bytes, |
| 95 | true, |
| 96 | None, |
| 97 | request, |
| 98 | )) |
| 99 | .expect_err("invalid PDF response must fail before raw preservation"); |
| 100 | assert!(error.to_string().contains(expected), "{error}"); |
| 101 | } |
| 102 | assert!( |
| 103 | !sessions.exists(), |
| 104 | "rejected bytes must not create artifacts" |
| 105 | ); |
| 106 | } |
| 107 | |
| 108 | #[tokio::test] |
| 109 | async fn fetched_pdf_missing_helper_is_a_failed_typed_outcome() { |
| 110 | let temporary = tempfile::tempdir().expect("tempdir"); |
| 111 | let missing = temporary.path().join("definitely-not-pdftotext"); |
| 112 | let error = extract_fetched_document( |
| 113 | Format::Text, |
| 114 | "https://example.com/document.pdf", |
| 115 | "application/pdf", |
| 116 | b"%PDF-1.7\n%%EOF", |
| 117 | true, |
| 118 | None, |
| 119 | PdfTextCommand::test(missing.as_os_str(), Duration::from_secs(1), None), |
| 120 | ) |
| 121 | .await |
| 122 | .expect_err("missing helper must fail the fetched PDF call"); |
| 123 | let payload = match &error { |
| 124 | ToolError::NotAvailable { message } => { |
| 125 | serde_json::from_str::<Value>(message).expect("structured unavailable payload") |
| 126 | } |
| 127 | other => panic!("unexpected error: {other:?}"), |
| 128 | }; |
| 129 | assert_eq!(payload["type"], "binary_unavailable"); |
| 130 | assert_eq!( |
| 131 | crate::tools::spec::ToolExecutionOutcome::from_legacy(Err(error)).status, |
| 132 | crate::tools::spec::ToolTerminalStatus::Failed |
| 133 | ); |
| 134 | } |
| 135 |