use crate::app::App;
use bytes::Bytes;
use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, Uri};
pub struct TestClient {
app: App,
}
pub struct TestRequest<'c> {
client: &'c TestClient,
method: Method,
uri: String,
headers: HeaderMap,
body: Bytes,
}
pub struct TestResponse {
status: StatusCode,
headers: HeaderMap,
body: Bytes,
}
impl TestClient {
pub fn new(app: App) -> Self {
Self { app }
}
pub fn request(&self, method: Method, uri: impl Into<String>) -> TestRequest<'_> {
TestRequest {
client: self,
method,
uri: uri.into(),
headers: HeaderMap::new(),
body: Bytes::new(),
}
}
pub fn get(&self, uri: impl Into<String>) -> TestRequest<'_> {
self.request(Method::GET, uri)
}
pub fn post(&self, uri: impl Into<String>) -> TestRequest<'_> {
self.request(Method::POST, uri)
}
pub fn put(&self, uri: impl Into<String>) -> TestRequest<'_> {
self.request(Method::PUT, uri)
}
pub fn delete(&self, uri: impl Into<String>) -> TestRequest<'_> {
self.request(Method::DELETE, uri)
}
}
impl<'c> TestRequest<'c> {
pub fn header(mut self, name: &'static str, value: &str) -> Self {
self.headers.insert(
HeaderName::from_static(name),
HeaderValue::from_str(value).expect("valid header value"),
);
self
}
pub fn body(mut self, body: impl Into<Bytes>) -> Self {
self.body = body.into();
self
}
pub async fn send(self) -> TestResponse {
let uri = self.uri.parse::<Uri>().expect("valid URI");
let res = self
.client
.app
.process(self.method, uri, self.headers, self.body)
.await;
let status = res.status;
let headers = res.headers;
let body = res.body.into_bytes().await.unwrap_or_default();
TestResponse {
status,
headers,
body,
}
}
}
impl TestResponse {
pub fn status(&self) -> StatusCode {
self.status
}
pub fn headers(&self) -> &http::HeaderMap {
&self.headers
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers.get(name).and_then(|v| v.to_str().ok())
}
pub fn body_bytes(&self) -> &Bytes {
&self.body
}
pub fn text(&self) -> String {
String::from_utf8_lossy(&self.body).into_owned()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::call::Call;
use crate::Churust;
fn app() -> App {
Churust::server()
.routing(|r| {
r.get("/", |_c: Call| async { "home" });
r.post("/echo", |mut c: Call| async move {
c.receive_text().await.unwrap_or_default()
});
})
.build()
}
#[tokio::test]
async fn get_returns_body_and_status() {
let client = TestClient::new(app());
let res = client.get("/").send().await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.text(), "home");
}
#[tokio::test]
async fn post_echoes_body() {
let client = TestClient::new(app());
let res = client.post("/echo").body("ping").send().await;
assert_eq!(res.text(), "ping");
}
#[tokio::test]
async fn missing_route_is_404() {
let client = TestClient::new(app());
let res = client.get("/nope").send().await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn collects_streamed_body() {
use crate::body::Body;
use crate::{Call, Churust};
use bytes::Bytes;
let app = Churust::server()
.routing(|r| {
r.get("/stream", |_c: Call| async {
let chunks = futures_util::stream::iter(vec![
Ok::<_, std::io::Error>(Bytes::from("foo")),
Ok(Bytes::from("bar")),
]);
crate::Response::stream("text/plain", Body::from_stream(chunks))
});
})
.build();
let res = TestClient::new(app).get("/stream").send().await;
assert_eq!(res.status(), http::StatusCode::OK);
assert_eq!(res.text(), "foobar");
}
}