use std::sync::{Arc, Mutex};
use blitz_net::Provider;
use blitz_traits::platform::{
FetchError, FetchHandler, FetchProvider, FetchRequest, FetchResponse, StatusCode, Url,
};
type Slot = Arc<Mutex<Option<Result<FetchResponse, FetchError>>>>;
#[derive(Default)]
struct Sink(Slot);
impl FetchHandler for Sink {
fn complete(self: Box<Self>, result: Result<FetchResponse, FetchError>) {
*self.0.lock().unwrap() = Some(result);
}
}
async fn fetch_one(url: &str) -> Result<FetchResponse, FetchError> {
let provider = Provider::new(None);
let slot: Slot = Arc::default();
provider.fetch(
FetchRequest::get(Url::parse(url).expect("test URL should parse")),
Box::new(Sink(slot.clone())),
);
for _ in 0..2_000 {
if let Some(answer) = slot.lock().unwrap().take() {
return answer;
}
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
panic!("the provider never answered for {url}");
}
#[tokio::test]
async fn a_data_url_answers_with_a_body_and_a_content_type() {
let response = fetch_one("data:text/plain,hello%20world")
.await
.expect("a data URL should succeed");
assert_eq!(response.status, StatusCode::OK);
assert_eq!(response.body.as_ref(), b"hello world");
assert_eq!(
response
.headers
.get(reqwest::header::CONTENT_TYPE)
.and_then(|value| value.to_str().ok()),
Some("text/plain"),
"the content type encoded in the URL is the response's"
);
}
#[tokio::test]
async fn a_base64_data_url_is_decoded() {
let response = fetch_one("data:application/json;base64,eyJvayI6dHJ1ZX0=")
.await
.expect("a base64 data URL should succeed");
assert_eq!(response.body.as_ref(), br#"{"ok":true}"#);
}
#[tokio::test]
async fn a_file_url_answers_with_the_file() {
let path = std::env::temp_dir().join("blitz-net-fetch-plain.txt");
std::fs::write(&path, b"on disk").unwrap();
let url = Url::from_file_path(&path).expect("a temp path should convert to a URL");
let response = fetch_one(url.as_str())
.await
.expect("an existing file should succeed");
assert_eq!(response.status, StatusCode::OK);
assert_eq!(response.body.as_ref(), b"on disk");
std::fs::remove_file(&path).ok();
}
#[tokio::test]
async fn a_file_url_with_a_space_in_the_name_is_decoded_before_opening() {
let path = std::env::temp_dir().join("blitz-net fetch spaced.txt");
std::fs::write(&path, b"spaces are fine").unwrap();
let url = Url::from_file_path(&path).expect("a temp path should convert to a URL");
assert!(
url.as_str().contains("%20"),
"the URL should be percent-encoded, or this test proves nothing: {url}"
);
let response = fetch_one(url.as_str())
.await
.expect("a file with a space should be found");
assert_eq!(response.body.as_ref(), b"spaces are fine");
std::fs::remove_file(&path).ok();
}
#[tokio::test]
async fn a_missing_file_is_a_failure_and_not_an_empty_response() {
let path = std::env::temp_dir().join("blitz-net-fetch-does-not-exist.txt");
std::fs::remove_file(&path).ok();
let url = Url::from_file_path(&path).unwrap();
let error = fetch_one(url.as_str())
.await
.expect_err("a missing file has no response");
assert!(
matches!(error, FetchError::Network(_)),
"expected a network-class failure, got {error:?}"
);
}
#[tokio::test]
async fn an_unservable_scheme_says_so_rather_than_hanging() {
let error = fetch_one("mailto:someone@example.com")
.await
.expect_err("mailto is not fetchable");
match error {
FetchError::UnsupportedScheme(scheme) => assert_eq!(scheme, "mailto"),
other => panic!("expected UnsupportedScheme, got {other:?}"),
}
}
#[tokio::test]
async fn concurrent_requests_do_not_cross_answers() {
let provider = Arc::new(Provider::new(None));
let slots: Vec<Slot> = (0..8).map(|_| Arc::default()).collect();
for (nth, slot) in slots.iter().enumerate() {
provider.fetch(
FetchRequest::get(Url::parse(&format!("data:text/plain,body-{nth}")).unwrap()),
Box::new(Sink(slot.clone())),
);
}
for _ in 0..2_000 {
if slots.iter().all(|slot| slot.lock().unwrap().is_some()) {
break;
}
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
}
for (nth, slot) in slots.iter().enumerate() {
let answer = slot.lock().unwrap().take().expect("every request answers");
let response = answer.expect("every data URL succeeds");
assert_eq!(
response.body.as_ref(),
format!("body-{nth}").as_bytes(),
"request {nth} received another request's body"
);
}
}