use axum::{
body::Body,
http::{Request, StatusCode},
Router,
};
use serde::{de::DeserializeOwned, Serialize};
use tower::ServiceExt;
pub struct TestClient {
app: Router,
}
impl TestClient {
pub fn new(app: Router) -> Self {
Self { app }
}
pub async fn get(&self, uri: &str) -> TestResponse {
self.request(Request::builder().uri(uri).body(Body::empty()).unwrap())
.await
}
pub async fn post<T: Serialize>(&self, uri: &str, body: &T) -> TestResponse {
let json_body = serde_json::to_string(body).unwrap();
self.request(
Request::builder()
.uri(uri)
.method("POST")
.header("content-type", "application/json")
.body(Body::from(json_body))
.unwrap(),
)
.await
}
pub async fn put<T: Serialize>(&self, uri: &str, body: &T) -> TestResponse {
let json_body = serde_json::to_string(body).unwrap();
self.request(
Request::builder()
.uri(uri)
.method("PUT")
.header("content-type", "application/json")
.body(Body::from(json_body))
.unwrap(),
)
.await
}
pub async fn patch<T: Serialize>(&self, uri: &str, body: &T) -> TestResponse {
let json_body = serde_json::to_string(body).unwrap();
self.request(
Request::builder()
.uri(uri)
.method("PATCH")
.header("content-type", "application/json")
.body(Body::from(json_body))
.unwrap(),
)
.await
}
pub async fn delete(&self, uri: &str) -> TestResponse {
self.request(
Request::builder()
.uri(uri)
.method("DELETE")
.body(Body::empty())
.unwrap(),
)
.await
}
pub async fn authorized_get(&self, uri: &str, token: &str) -> TestResponse {
self.request(
Request::builder()
.uri(uri)
.header("authorization", format!("Bearer {}", token))
.body(Body::empty())
.unwrap(),
)
.await
}
pub async fn authorized_post<T: Serialize>(
&self,
uri: &str,
token: &str,
body: &T,
) -> TestResponse {
let json_body = serde_json::to_string(body).unwrap();
self.request(
Request::builder()
.uri(uri)
.method("POST")
.header("content-type", "application/json")
.header("authorization", format!("Bearer {}", token))
.body(Body::from(json_body))
.unwrap(),
)
.await
}
async fn request(&self, req: Request<Body>) -> TestResponse {
let response = self
.app
.clone()
.oneshot(req)
.await
.expect("Failed to send request");
let status = response.status();
let headers = response.headers().clone();
let body_bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("Failed to read response body");
TestResponse {
status,
headers,
body: body_bytes.to_vec(),
}
}
}
pub struct TestResponse {
pub status: StatusCode,
pub headers: axum::http::HeaderMap,
body: Vec<u8>,
}
impl TestResponse {
pub fn text(&self) -> String {
String::from_utf8(self.body.clone()).expect("Response body is not valid UTF-8")
}
pub fn json<T: DeserializeOwned>(&self) -> T {
serde_json::from_slice(&self.body).expect("Failed to deserialize JSON response")
}
pub fn assert_status(&self, expected: StatusCode) -> &Self {
assert_eq!(
self.status, expected,
"Expected status {}, got {}. Body: {}",
expected,
self.status,
self.text()
);
self
}
pub fn assert_text_contains(&self, expected: &str) -> &Self {
let body = self.text();
assert!(
body.contains(expected),
"Expected body to contain '{}', got: {}",
expected, body
);
self
}
pub fn is_success(&self) -> bool {
self.status.is_success()
}
}
#[cfg(feature = "db-tests")]
pub mod db {
use sqlx::PgPool;
pub async fn test_pool() -> PgPool {
let database_url = std::env::var("TEST_DATABASE_URL")
.unwrap_or_else(|_| "postgres://postgres:postgres@localhost/rapid_rs_test".to_string());
PgPool::connect(&database_url)
.await
.expect("Failed to connect to test database")
}
pub async fn cleanup(pool: &PgPool) {
sqlx::query("TRUNCATE TABLE users CASCADE")
.execute(pool)
.await
.ok(); }
}
#[cfg(test)]
mod tests {
use super::*;
use axum::{routing::get, Json, Router};
use serde_json::json;
async fn hello() -> Json<serde_json::Value> {
Json(json!({"message": "Hello, World!"}))
}
async fn echo(Json(body): Json<serde_json::Value>) -> Json<serde_json::Value> {
Json(body)
}
#[tokio::test]
async fn test_client_get() {
let app = Router::new().route("/hello", get(hello));
let client = TestClient::new(app);
let response = client.get("/hello").await;
response.assert_status(StatusCode::OK);
let json: serde_json::Value = response.json();
assert_eq!(json["message"], "Hello, World!");
}
#[tokio::test]
async fn test_client_post() {
let app = Router::new().route("/echo", axum::routing::post(echo));
let client = TestClient::new(app);
let body = json!({"test": "data"});
let response = client.post("/echo", &body).await;
response.assert_status(StatusCode::OK);
let json: serde_json::Value = response.json();
assert_eq!(json["test"], "data");
}
}