use std::sync::Arc;
use axum::Router;
use axum::body::Body;
use axum::http::{Method, Request};
use axum::response::Response;
use tower::Service;
use super::request::TestRequest;
#[derive(Clone)]
pub struct TestApp {
inner: Arc<Inner>,
}
struct Inner {
router: Router,
#[cfg(feature = "auth")]
sessions: Option<super::session::TestSessions>,
}
impl TestApp {
#[must_use]
pub fn new(app: crate::Application<()>) -> Self {
Self::from_router(app.into_router())
}
#[must_use]
pub fn with_state<S>(app: crate::Application<S>, state: S) -> Self
where
S: crate::RouterState,
{
Self::from_router(app.into_router().with_state(state))
}
#[must_use]
pub fn from_router(router: Router) -> Self {
Self {
inner: Arc::new(Inner {
router,
#[cfg(feature = "auth")]
sessions: None,
}),
}
}
}
impl TestApp {
#[cfg(feature = "auth")]
#[must_use]
pub fn with_sessions(self, sessions: super::session::TestSessions) -> Self {
Self {
inner: Arc::new(Inner {
router: self.inner.router.clone(),
sessions: Some(sessions),
}),
}
}
#[cfg(feature = "auth")]
pub(crate) fn sessions(&self) -> Option<&super::session::TestSessions> {
self.inner.sessions.as_ref()
}
#[must_use]
pub fn get(&self, path: impl Into<String>) -> TestRequest {
self.request(Method::GET, path)
}
#[must_use]
pub fn post(&self, path: impl Into<String>) -> TestRequest {
self.request(Method::POST, path)
}
#[must_use]
pub fn put(&self, path: impl Into<String>) -> TestRequest {
self.request(Method::PUT, path)
}
#[must_use]
pub fn patch(&self, path: impl Into<String>) -> TestRequest {
self.request(Method::PATCH, path)
}
#[must_use]
pub fn delete(&self, path: impl Into<String>) -> TestRequest {
self.request(Method::DELETE, path)
}
#[must_use]
pub fn head(&self, path: impl Into<String>) -> TestRequest {
self.request(Method::HEAD, path)
}
#[must_use]
pub fn request(&self, method: Method, path: impl Into<String>) -> TestRequest {
TestRequest::new(self.clone(), method, path.into())
}
}
impl TestApp {
pub(crate) async fn dispatch(&self, request: Request<Body>) -> Response {
let mut router = self.inner.router.clone();
std::future::poll_fn(|cx| <Router as Service<Request<Body>>>::poll_ready(&mut router, cx))
.await
.expect("axum Router::poll_ready is infallible");
router
.call(request)
.await
.expect("axum Router::call is infallible")
}
pub async fn serve(&self) -> std::io::Result<TestServer> {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
let address = listener.local_addr()?;
let router = self.inner.router.clone();
let task = tokio::spawn(async move {
let _ = axum::serve(listener, router.into_make_service()).await;
});
Ok(TestServer { address, task })
}
}
impl std::fmt::Debug for TestApp {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut out = formatter.debug_struct("TestApp");
#[cfg(feature = "auth")]
out.field("has_sessions", &self.inner.sessions.is_some());
out.finish_non_exhaustive()
}
}
#[derive(Debug)]
pub struct TestServer {
address: std::net::SocketAddr,
task: tokio::task::JoinHandle<()>,
}
impl TestServer {
#[must_use]
pub fn address(&self) -> std::net::SocketAddr {
self.address
}
#[must_use]
pub fn base_url(&self) -> String {
format!("http://{}", self.address)
}
#[must_use]
pub fn ws_url(&self, path: &str) -> String {
format!("ws://{}{path}", self.address)
}
}
impl Drop for TestServer {
fn drop(&mut self) {
self.task.abort();
}
}
pub trait IntoTestApp {
fn into_test_app(self) -> TestApp;
}
impl IntoTestApp for TestApp {
fn into_test_app(self) -> TestApp {
self
}
}
impl IntoTestApp for Router {
fn into_test_app(self) -> TestApp {
TestApp::from_router(self)
}
}
impl IntoTestApp for crate::Application<()> {
fn into_test_app(self) -> TestApp {
TestApp::new(self)
}
}