use alux_http::{HttpMethod, HttpStatus};
use alux_http_parts::{DirectRequest, DirectResponse};
use core::future::Future;
pub trait AnswerAlg {
fn answer(&self, request: DirectRequest) -> impl Future<Output = DirectResponse> + Send;
}
#[derive(Debug, Clone)]
pub struct Exchange {
pub what: &'static str,
pub request: DirectRequest,
pub status: HttpStatus,
pub body: Option<&'static str>,
pub header: Option<(&'static str, &'static str)>,
}
impl Exchange {
fn asked(what: &'static str, method: HttpMethod, path: &str, status: HttpStatus) -> Self {
Self { what, request: DirectRequest::new(method, path), status, body: None, header: None }
}
#[must_use]
fn sending(mut self, name: &str, value: &str) -> Self {
self.request = self.request.with_header(name, value);
self
}
fn sent(
what: &'static str,
method: HttpMethod,
path: &str,
content_type: &str,
body: &'static str,
status: HttpStatus,
) -> Self {
let request = DirectRequest::new(method, path).with_header("content-type", content_type).with_body(body);
Self { what, request, status, body: None, header: None }
}
#[must_use]
fn answering(mut self, body: &'static str) -> Self {
self.body = Some(body);
self
}
#[must_use]
fn carrying(mut self, name: &'static str, value: &'static str) -> Self {
self.header = Some((name, value));
self
}
fn disagreements(&self, answer: &DirectResponse) -> Vec<String> {
let mut found = Vec::new();
let what = self.what;
if answer.status() != self.status {
found.push(format!("{what}: answered {} where {} was stated", answer.status().code(), self.status.code()));
}
if let Some(body) = self.body
&& answer.text() != body
{
found.push(format!("{what}: answered `{}` where `{body}` was stated", answer.text()));
}
if let Some((name, value)) = self.header
&& answer.header(name) != Some(value)
{
found.push(format!(
"{what}: answered `{name}: {}` where `{name}: {value}` was stated",
answer.header(name).unwrap_or("nothing")
));
}
found
}
}
pub fn exchanges() -> Vec<Exchange> {
vec![
Exchange::asked("a reading as it stands", HttpMethod::Get, "/items", HttpStatus::OK).answering("[7]"),
Exchange::asked("a path binding one segment", HttpMethod::Get, "/item/1", HttpStatus::OK).answering("7"),
Exchange::asked("what a failure means", HttpMethod::Get, "/item/9", HttpStatus::NOT_FOUND)
.answering("no such reading"),
Exchange::sent(
"a body read as a document, under a declared status",
HttpMethod::Post,
"/items",
"application/json",
"7",
HttpStatus::new(201),
)
.answering("7"),
Exchange::sent(
"a body read as a form",
HttpMethod::Put,
"/items",
"application/x-www-form-urlencoded",
"value=7",
HttpStatus::OK,
)
.answering("7"),
Exchange::sent(
"a body taken as it arrived",
HttpMethod::Patch,
"/items",
"text/plain",
"seven",
HttpStatus::OK,
)
.answering("noted seven"),
Exchange::asked("an answer with no body", HttpMethod::Delete, "/items", HttpStatus::NO_CONTENT).answering(""),
Exchange::asked("a redirect", HttpMethod::Get, "/home", HttpStatus::SEE_OTHER).carrying("location", "/items"),
Exchange::asked("a page", HttpMethod::Get, "/page", HttpStatus::OK)
.answering("<p>7</p>")
.carrying("content-type", "text/html; charset=utf-8"),
Exchange::asked("the readings as stored", HttpMethod::Get, "/stored", HttpStatus::OK).answering("seven"),
Exchange::asked("what the cookies a caller sent state", HttpMethod::Get, "/session", HttpStatus::OK)
.sending("cookie", "session=abc; theme=dark")
.answering("known as abc"),
Exchange::asked("a header an answer carries", HttpMethod::Get, "/cached", HttpStatus::OK)
.answering("[7]")
.carrying("cache-control", "max-age=60"),
Exchange::asked("what the headers a caller sent state", HttpMethod::Get, "/agent", HttpStatus::OK)
.sending("user-agent", "probe")
.answering("sent by probe"),
]
}
pub async fn expect<Answers>(answers: &Answers) -> Result<(), Vec<String>>
where
Answers: AnswerAlg,
{
let mut found = Vec::new();
for exchange in exchanges() {
let answer = answers.answer(exchange.request.clone()).await.collected().await;
found.extend(exchange.disagreements(&answer));
}
if found.is_empty() { Ok(()) } else { Err(found) }
}
pub fn stream_exchanges() -> Vec<Exchange> {
vec![
Exchange::asked("a body produced a piece at a time", HttpMethod::Get, "/ticks", HttpStatus::OK)
.answering("onetwothree"),
]
}
pub async fn expect_streaming<Answers>(answers: &Answers) -> Result<(), Vec<String>>
where
Answers: AnswerAlg,
{
let mut found = Vec::new();
for exchange in stream_exchanges() {
let answer = answers.answer(exchange.request.clone()).await.collected().await;
found.extend(exchange.disagreements(&answer));
}
if found.is_empty() { Ok(()) } else { Err(found) }
}
const BOUNDARY: &str = "alux";
const PARTS: &str = "--alux\r\nContent-Disposition: form-data; name=\"one\"\r\n\r\n1\r\n\
--alux\r\nContent-Disposition: form-data; name=\"two\"\r\n\r\n2\r\n--alux--\r\n";
pub fn multipart_exchanges() -> Vec<Exchange> {
vec![
Exchange::sent(
"a body arriving as parts",
HttpMethod::Post,
"/upload",
&format!("multipart/form-data; boundary={BOUNDARY}"),
PARTS,
HttpStatus::OK,
)
.answering("one=1,two=2"),
]
}
pub async fn expect_multipart<Answers>(answers: &Answers) -> Result<(), Vec<String>>
where
Answers: AnswerAlg,
{
let mut found = Vec::new();
for exchange in multipart_exchanges() {
let answer = answers.answer(exchange.request.clone()).await.collected().await;
found.extend(exchange.disagreements(&answer));
}
if found.is_empty() { Ok(()) } else { Err(found) }
}