#![allow(dead_code)]
use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anypost::transport::{HttpRequest, HttpResponse, Method, Sleeper, Transport, TransportError};
use anypost::{Client, ClientBuilder};
use async_trait::async_trait;
use serde_json::Value;
#[derive(Clone, Debug)]
pub struct Recorded {
pub method: Method,
pub url: String,
pub headers: Vec<(String, String)>,
pub body: Option<Vec<u8>>,
}
impl Recorded {
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, value)| value.as_str())
}
pub fn json(&self) -> Value {
let bytes = self.body.as_ref().expect("request had a body");
serde_json::from_slice(bytes).expect("request body was JSON")
}
pub fn path(&self) -> &str {
self.url.split('?').next().unwrap_or(&self.url)
}
pub fn query(&self, name: &str) -> Option<String> {
let query = self.url.split_once('?')?.1;
for pair in query.split('&') {
if let Some((key, value)) = pair.split_once('=') {
if key == name {
return Some(value.to_string());
}
}
}
None
}
}
pub enum Canned {
Response(HttpResponse),
Error(TransportError),
}
pub struct MockTransport {
requests: Mutex<Vec<Recorded>>,
responses: Mutex<VecDeque<Canned>>,
}
impl MockTransport {
fn new(responses: Vec<Canned>) -> Self {
Self {
requests: Mutex::new(Vec::new()),
responses: Mutex::new(responses.into_iter().collect()),
}
}
pub fn requests(&self) -> Vec<Recorded> {
self.requests.lock().unwrap().clone()
}
pub fn request_count(&self) -> usize {
self.requests.lock().unwrap().len()
}
pub fn last(&self) -> Recorded {
self.requests
.lock()
.unwrap()
.last()
.cloned()
.expect("a request was made")
}
pub fn first(&self) -> Recorded {
self.requests
.lock()
.unwrap()
.first()
.cloned()
.expect("a request was made")
}
}
#[async_trait]
impl Transport for MockTransport {
async fn execute(&self, request: HttpRequest) -> Result<HttpResponse, TransportError> {
self.requests.lock().unwrap().push(Recorded {
method: request.method,
url: request.url,
headers: request.headers,
body: request.body,
});
match self.responses.lock().unwrap().pop_front() {
Some(Canned::Response(response)) => Ok(response),
Some(Canned::Error(error)) => Err(error),
None => panic!("MockTransport ran out of queued responses"),
}
}
}
struct NoopSleeper;
#[async_trait]
impl Sleeper for NoopSleeper {
async fn sleep(&self, _duration: Duration) {}
fn jitter(&self) -> f64 {
1.0
}
}
pub fn json(status: u16, body: Value) -> Canned {
Canned::Response(HttpResponse {
status,
headers: Vec::new(),
body: serde_json::to_vec(&body).unwrap(),
})
}
pub fn json_headers(status: u16, body: Value, headers: &[(&str, &str)]) -> Canned {
Canned::Response(HttpResponse {
status,
headers: headers
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
body: serde_json::to_vec(&body).unwrap(),
})
}
pub fn no_content() -> Canned {
Canned::Response(HttpResponse {
status: 204,
headers: Vec::new(),
body: Vec::new(),
})
}
pub fn network_error() -> Canned {
Canned::Error(TransportError {
message: "connection reset".to_string(),
timeout: false,
})
}
pub fn client(responses: Vec<Canned>) -> (Client, Arc<MockTransport>) {
client_with(responses, |b| b)
}
pub fn client_with(
responses: Vec<Canned>,
configure: impl FnOnce(ClientBuilder) -> ClientBuilder,
) -> (Client, Arc<MockTransport>) {
let transport = Arc::new(MockTransport::new(responses));
let builder = Client::builder()
.api_key("ap_test_key")
.base_url("https://api.test/v1")
.transport(transport.clone())
.sleeper(Arc::new(NoopSleeper));
let client = configure(builder).build().expect("client builds");
(client, transport)
}