use std::sync::Arc;
use serde::de::DeserializeOwned;
use serde_json::Value;
use rainier_container::{scope_facade_application, Application, FacadeScope};
use rainier_http::{Method, Request, Response, StatusCode};
use rainier_server::Kernel;
use rainier_support::Result;
pub struct TestApp {
app: Arc<Application>,
kernel: Arc<Kernel>,
_scope: FacadeScope,
defaults: Vec<(String, String)>,
}
impl TestApp {
pub fn new(app: Arc<Application>) -> Result<Self> {
let kernel = app.resolve::<Kernel>()?;
let scope = scope_facade_application(Arc::clone(&app));
Ok(Self { app, kernel, _scope: scope, defaults: Vec::new() })
}
pub fn app(&self) -> &Arc<Application> {
&self.app
}
pub fn resolve<T: Send + Sync + 'static>(&self) -> Result<Arc<T>> {
self.app.resolve::<T>()
}
#[must_use = "this returns a configured harness rather than configuring in place"]
pub fn with_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.defaults.push((name.into(), value.into()));
self
}
#[must_use = "this returns a configured harness rather than configuring in place"]
pub fn with_token(self, token: impl std::fmt::Display) -> Self {
self.with_header("authorization", format!("Bearer {token}"))
}
pub async fn get(&self, uri: &str) -> TestResponse {
self.send(self.request(Method::GET, uri).build()).await
}
pub async fn delete(&self, uri: &str) -> TestResponse {
self.send(self.request(Method::DELETE, uri).build()).await
}
pub async fn post(&self, uri: &str, body: &impl serde::Serialize) -> TestResponse {
self.send(self.request(Method::POST, uri).json(body).build()).await
}
pub async fn put(&self, uri: &str, body: &impl serde::Serialize) -> TestResponse {
self.send(self.request(Method::PUT, uri).json(body).build()).await
}
pub async fn patch(&self, uri: &str, body: &impl serde::Serialize) -> TestResponse {
self.send(self.request(Method::PATCH, uri).json(body).build()).await
}
pub async fn post_empty(&self, uri: &str) -> TestResponse {
self.send(self.request(Method::POST, uri).build()).await
}
pub fn request(&self, method: Method, uri: &str) -> rainier_http::RequestBuilder {
let mut builder = Request::builder().method(method).uri(uri);
for (name, value) in &self.defaults {
builder = builder.header(name.as_str(), value.as_str());
}
builder
}
pub async fn send(&self, request: Request) -> TestResponse {
TestResponse::of(self.kernel.handle_request(request).await).await
}
}
pub struct TestResponse {
status: StatusCode,
headers: Vec<(String, String)>,
body: String,
}
impl TestResponse {
pub async fn of(response: Response) -> Self {
let status = response.status();
let headers = response
.headers()
.iter()
.filter_map(|(name, value)| {
Some((name.as_str().to_string(), value.to_str().ok()?.to_string()))
})
.collect();
let body = response.into_string().await.unwrap_or_default();
Self { status, headers, body }
}
pub fn status(&self) -> StatusCode {
self.status
}
pub fn text(&self) -> &str {
&self.body
}
pub fn header(&self, name: &str) -> Option<&str> {
self.headers
.iter()
.find(|(header, _)| header.eq_ignore_ascii_case(name))
.map(|(_, value)| value.as_str())
}
pub fn json(&self) -> Value {
serde_json::from_str(&self.body).unwrap_or_else(|e| {
panic!(
"the response body is not JSON: {e}\n status: {}\n body: {}",
self.status, self.body
)
})
}
pub fn json_as<T: DeserializeOwned>(&self) -> T {
serde_json::from_str(&self.body).unwrap_or_else(|e| {
panic!("the response body is not the expected shape: {e}\n body: {}", self.body)
})
}
pub fn assert_status(&self, expected: StatusCode) -> &Self {
assert_eq!(
self.status, expected,
"expected {expected}, got {}\n body: {}",
self.status, self.body
);
self
}
pub fn assert_ok(&self) -> &Self {
assert!(
self.status.is_success(),
"expected a 2xx, got {}\n body: {}",
self.status,
self.body
);
self
}
pub fn assert_created(&self) -> &Self {
self.assert_status(StatusCode::CREATED)
}
pub fn assert_no_content(&self) -> &Self {
self.assert_status(StatusCode::NO_CONTENT)
}
pub fn assert_not_found(&self) -> &Self {
self.assert_status(StatusCode::NOT_FOUND)
}
pub fn assert_unauthorized(&self) -> &Self {
self.assert_status(StatusCode::UNAUTHORIZED)
}
pub fn assert_forbidden(&self) -> &Self {
self.assert_status(StatusCode::FORBIDDEN)
}
pub fn assert_invalid(&self) -> &Self {
self.assert_status(StatusCode::UNPROCESSABLE_ENTITY)
}
pub fn assert_json_path(&self, path: &str, expected: impl Into<Value>) -> &Self {
let expected = expected.into();
let document = self.json();
match json_path(&document, path) {
Some(actual) => assert_eq!(
*actual, expected,
"at `{path}`: expected {expected}, got {actual}\n body: {}",
self.body
),
None => panic!("`{path}` is not in the response\n body: {}", self.body),
}
self
}
pub fn assert_json_missing(&self, path: &str) -> &Self {
assert!(
json_path(&self.json(), path).is_none(),
"`{path}` should not be in the response\n body: {}",
self.body
);
self
}
pub fn assert_contains(&self, text: &str) -> &Self {
assert!(
self.body.contains(text),
"the body does not contain {text:?}\n body: {}",
self.body
);
self
}
pub fn assert_header(&self, name: &str, expected: &str) -> &Self {
match self.header(name) {
Some(actual) => assert_eq!(actual, expected, "header `{name}`"),
None => panic!("`{name}` is not on the response — it has {:?}", self.header_names()),
}
self
}
pub fn assert_header_missing(&self, name: &str) -> &Self {
assert!(self.header(name).is_none(), "`{name}` should not be on the response");
self
}
fn header_names(&self) -> Vec<&str> {
self.headers.iter().map(|(name, _)| name.as_str()).collect()
}
}
fn json_path<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
let mut current = value;
for segment in path.split('.') {
current = match current {
Value::Array(items) => items.get(segment.parse::<usize>().ok()?)?,
other => other.get(segment)?,
};
}
Some(current)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn document() -> Value {
json!({
"database": { "status": "ok", "latency_ms": 3 },
"data": [{ "title": "First" }, { "title": "Second" }],
"nothing": null,
})
}
#[test]
fn a_dotted_path_walks_objects_and_arrays() {
let value = document();
assert_eq!(json_path(&value, "database.status"), Some(&json!("ok")));
assert_eq!(json_path(&value, "data.1.title"), Some(&json!("Second")));
assert_eq!(json_path(&value, "database.latency_ms"), Some(&json!(3)));
}
#[test]
fn an_absent_path_is_none_rather_than_a_panic() {
let value = document();
assert_eq!(json_path(&value, "database.nope"), None);
assert_eq!(json_path(&value, "data.9.title"), None);
assert_eq!(json_path(&value, "data.title"), None, "an array indexed by a name");
}
#[test]
fn a_null_is_present_and_not_missing() {
assert_eq!(json_path(&document(), "nothing"), Some(&json!(null)));
}
#[tokio::test]
async fn a_response_is_read_once_and_asserted_many_times() {
let response = TestResponse::of(
Response::ok(r#"{"database":{"status":"ok"}}"#)
.with_header("content-type", "application/json"),
)
.await;
response
.assert_ok()
.assert_status(StatusCode::OK)
.assert_json_path("database.status", "ok")
.assert_json_missing("database.error")
.assert_header("content-type", "application/json")
.assert_contains("status");
assert_eq!(response.json()["database"]["status"], "ok");
}
#[tokio::test]
#[should_panic(expected = "expected 404 Not Found, got 200 OK")]
async fn a_failed_status_assertion_prints_the_body() {
TestResponse::of(Response::ok("hello")).await.assert_not_found();
}
#[tokio::test]
#[should_panic(expected = "`database.status` is not in the response")]
async fn a_missing_path_says_which_path() {
TestResponse::of(Response::ok("{}")).await.assert_json_path("database.status", "ok");
}
}