use ccdown::download::{fetch_record, RecordOptions};
use flate2::write::GzEncoder;
use flate2::Compression;
use std::io::Write;
use wiremock::matchers::{header, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn warc_gz_member(body: &[u8]) -> Vec<u8> {
let http = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/pdf\r\nContent-Length: {}\r\n\r\n",
body.len()
);
let block_len = http.len() + body.len();
let warc = format!(
"WARC/1.0\r\nWARC-Type: response\r\nWARC-Target-URI: http://example.com/a.pdf\r\nContent-Length: {block_len}\r\n\r\n"
);
let mut record = Vec::new();
record.extend_from_slice(warc.as_bytes());
record.extend_from_slice(http.as_bytes());
record.extend_from_slice(body);
record.extend_from_slice(b"\r\n\r\n");
let mut enc = GzEncoder::new(Vec::new(), Compression::default());
enc.write_all(&record).unwrap();
enc.finish().unwrap()
}
fn opts(server_url: &str) -> RecordOptions {
RecordOptions {
max_retries: 0,
base_url: Some(format!("{server_url}/")),
..Default::default()
}
}
#[tokio::test]
async fn fetches_pdf_record_from_warc() {
let pdf = b"%PDF-1.4 fake pdf bytes";
let mut payload = warc_gz_member(pdf);
payload.extend_from_slice(&warc_gz_member(b"%PDF decoy"));
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/crawl-data/test.warc.gz"))
.and(header("Range", "bytes=0-"))
.respond_with(ResponseTemplate::new(206).set_body_bytes(payload))
.mount(&server)
.await;
let record = fetch_record("crawl-data/test.warc.gz", 0, &opts(&server.uri()))
.await
.unwrap();
assert_eq!(record.body, pdf);
assert_eq!(record.warc_headers.get("warc-type").unwrap(), "response");
assert_eq!(
record.http_headers.get("content-type").unwrap(),
"application/pdf"
);
}
#[tokio::test]
async fn truncated_member_is_an_error() {
let full = warc_gz_member(b"%PDF-1.4 something");
let truncated = full[..full.len() / 2].to_vec();
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/t.warc.gz"))
.respond_with(ResponseTemplate::new(206).set_body_bytes(truncated))
.mount(&server)
.await;
let err = fetch_record("t.warc.gz", 0, &opts(&server.uri())).await;
assert!(err.is_err());
}
#[tokio::test]
async fn record_over_max_bytes_is_an_error() {
let body = vec![b'x'; 64 * 1024];
let payload = warc_gz_member(&body);
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/big.warc.gz"))
.respond_with(ResponseTemplate::new(206).set_body_bytes(payload))
.mount(&server)
.await;
let options = RecordOptions {
max_bytes: 1024,
..opts(&server.uri())
};
assert!(fetch_record("big.warc.gz", 0, &options).await.is_err());
}
#[tokio::test]
async fn unrecoverable_status_is_reported() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/gone.warc.gz"))
.respond_with(ResponseTemplate::new(404))
.mount(&server)
.await;
let err = fetch_record("gone.warc.gz", 0, &opts(&server.uri()))
.await
.unwrap_err();
assert!(err.is_unrecoverable());
}
#[tokio::test]
async fn sends_range_header_for_nonzero_offset() {
let body = b"%PDF-1.4 at offset";
let payload = warc_gz_member(body);
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/off.warc.gz"))
.and(header("Range", "bytes=12345-"))
.respond_with(ResponseTemplate::new(206).set_body_bytes(payload))
.mount(&server)
.await;
let record = fetch_record("off.warc.gz", 12345, &opts(&server.uri()))
.await
.unwrap();
assert_eq!(record.body, body);
}