use std::sync::Arc;
use serde::de::DeserializeOwned;
use serde::Serialize;
use tower::ServiceExt;
use wabot_feature_rest_controller::axum::body::Body;
use wabot_feature_rest_controller::axum::http::{HeaderMap, Request, StatusCode};
use wabot_feature_rest_controller::axum::Router;
use wabot_feature_rest_controller::rest_app;
#[derive(Clone)]
pub struct RestHarness {
router: Router,
default_headers: Arc<Vec<(String, String)>>,
}
impl RestHarness {
pub fn new(router: Router) -> Self {
Self {
router,
default_headers: Arc::new(Vec::new()),
}
}
pub fn with_header(&self, name: impl Into<String>, value: impl Into<String>) -> Self {
let mut headers = (*self.default_headers).clone();
headers.push((name.into(), value.into()));
Self {
router: self.router.clone(),
default_headers: Arc::new(headers),
}
}
pub fn with_bearer(&self, token: impl std::fmt::Display) -> Self {
self.with_header("authorization", format!("Bearer {token}"))
}
pub fn with_cookie(&self, name: &str, value: impl std::fmt::Display) -> Self {
self.with_header("cookie", format!("{name}={value}"))
}
pub fn get(&self, path: &str) -> RequestBuilder {
self.request("GET", path)
}
pub fn post(&self, path: &str) -> RequestBuilder {
self.request("POST", path)
}
pub fn put(&self, path: &str) -> RequestBuilder {
self.request("PUT", path)
}
pub fn delete(&self, path: &str) -> RequestBuilder {
self.request("DELETE", path)
}
pub fn request(&self, method: &str, path: &str) -> RequestBuilder {
RequestBuilder {
router: self.router.clone(),
method: method.to_string(),
path: path.to_string(),
query: Vec::new(),
headers: (*self.default_headers).clone(),
body: None,
}
}
}
pub struct RequestBuilder {
router: Router,
method: String,
path: String,
query: Vec<(String, String)>,
headers: Vec<(String, String)>,
body: Option<String>,
}
impl RequestBuilder {
pub fn json<T: Serialize>(mut self, body: &T) -> Self {
self.body = Some(serde_json::to_string(body).expect("a serializable body"));
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.body = Some(body.into());
self
}
pub fn query(mut self, key: &str, value: impl std::fmt::Display) -> Self {
self.query.push((key.into(), value.to_string()));
self
}
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((name.into(), value.into()));
self
}
pub fn bearer(self, token: impl std::fmt::Display) -> Self {
self.header("authorization", format!("Bearer {token}"))
}
pub async fn send(self) -> TestResponse {
let mut uri = self.path.clone();
if !self.query.is_empty() {
let encoded: Vec<String> = self
.query
.iter()
.map(|(k, v)| format!("{}={}", encode(k), encode(v)))
.collect();
uri = format!("{uri}?{}", encoded.join("&"));
}
let mut request = Request::builder().method(self.method.as_str()).uri(&uri);
let has_content_type = self
.headers
.iter()
.any(|(name, _)| name.eq_ignore_ascii_case("content-type"));
for (name, value) in &self.headers {
request = request.header(name, value);
}
if self.body.is_some() && !has_content_type {
request = request.header("content-type", "application/json");
}
let request = request
.body(self.body.map(Body::from).unwrap_or_else(Body::empty))
.expect("a valid request");
let response = rest_app(self.router)
.oneshot(request)
.await
.expect("the service should not fail outright");
let status = response.status();
let headers = response.headers().clone();
let bytes =
wabot_feature_rest_controller::axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("a readable body");
TestResponse {
status,
headers,
body: String::from_utf8_lossy(&bytes).into_owned(),
}
}
}
#[derive(Debug, Clone)]
pub struct TestResponse {
pub status: StatusCode,
pub headers: HeaderMap,
pub body: String,
}
impl TestResponse {
pub fn json<T: DeserializeOwned>(&self) -> T {
serde_json::from_str(&self.body).unwrap_or_else(|error| {
panic!(
"expected a {} body, got HTTP {} with {:?} ({error})",
std::any::type_name::<T>(),
self.status,
self.body
)
})
}
pub fn value(&self) -> serde_json::Value {
self.json()
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers.get(name).and_then(|v| v.to_str().ok())
}
pub fn is_success(&self) -> bool {
self.status.is_success()
}
pub fn assert_status(&self, expected: StatusCode) -> &Self {
assert_eq!(
self.status, expected,
"expected HTTP {expected}, got {} with body {:?}",
self.status, self.body
);
self
}
pub fn assert_ok(&self) -> &Self {
self.assert_status(StatusCode::OK)
}
}
fn encode(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for byte in value.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(byte as char)
}
b' ' => out.push_str("%20"),
other => out.push_str(&format!("%{other:02X}")),
}
}
out
}