Skip to main content

karbon_framework/testing/
mod.rs

1use std::net::SocketAddr;
2use tokio::net::TcpListener;
3
4use crate::config::Config;
5use crate::db::{Database, DbPool};
6use crate::http::AppState;
7use crate::mail::Mailer;
8
9
10/// Test application helper. Boots the app on a random port for integration tests.
11///
12/// ```ignore
13/// #[tokio::test]
14/// async fn test_health() {
15///     let app = TestApp::spawn(build_router()).await;
16///
17///     let res = app.get("/health").await;
18///     assert_eq!(res.status(), 200);
19///
20///     let body: serde_json::Value = res.json().await;
21///     assert_eq!(body["status"], "ok");
22/// }
23/// ```
24pub struct TestApp {
25    pub addr: SocketAddr,
26    pub pool: DbPool,
27    pub client: reqwest::Client,
28}
29
30impl TestApp {
31    /// Spawn the app on a random port. Requires DATABASE_URL or DB_* env vars.
32    pub async fn spawn(router: axum::Router<AppState>) -> Self {
33        dotenvy::dotenv().ok();
34        let config = Config::from_env();
35        let db = Database::connect(&config)
36            .await
37            .expect("Failed to connect to test database");
38
39        let pool = db.pool().clone();
40
41        let mailer = if !config.smtp_host.is_empty() && !config.smtp_user.is_empty() {
42            Mailer::new(&config).ok()
43        } else {
44            None
45        };
46
47        let state = AppState {
48            db,
49            config,
50            mailer,
51            role_hierarchy: crate::security::default_hierarchy(),
52            #[cfg(feature = "templates")]
53            templates: crate::template::TemplateEngine::empty(),
54        };
55
56        let router = router.with_state(state);
57
58        let listener = TcpListener::bind("127.0.0.1:0")
59            .await
60            .expect("Failed to bind test listener");
61        let addr = listener.local_addr().unwrap();
62
63        tokio::spawn(async move {
64            if let Err(e) = axum::serve(
65                listener,
66                router.into_make_service_with_connect_info::<SocketAddr>(),
67            )
68            .await
69            {
70                tracing::error!("Test server error: {e}");
71            }
72        });
73
74        let client = reqwest::Client::builder()
75            .redirect(reqwest::redirect::Policy::none())
76            .build()
77            .expect("Failed to build HTTP client");
78
79        Self { addr, pool, client }
80    }
81
82    /// Base URL for the test server
83    pub fn url(&self, path: &str) -> String {
84        format!("http://{}{}", self.addr, path)
85    }
86
87    /// GET request
88    pub async fn get(&self, path: &str) -> reqwest::Response {
89        self.client
90            .get(&self.url(path))
91            .send()
92            .await
93            .expect("Failed to send GET request")
94    }
95
96    /// POST request with JSON body
97    pub async fn post_json<T: serde::Serialize>(&self, path: &str, body: &T) -> reqwest::Response {
98        self.client
99            .post(&self.url(path))
100            .json(body)
101            .send()
102            .await
103            .expect("Failed to send POST request")
104    }
105
106    /// PUT request with JSON body
107    pub async fn put_json<T: serde::Serialize>(&self, path: &str, body: &T) -> reqwest::Response {
108        self.client
109            .put(&self.url(path))
110            .json(body)
111            .send()
112            .await
113            .expect("Failed to send PUT request")
114    }
115
116    /// DELETE request
117    pub async fn delete(&self, path: &str) -> reqwest::Response {
118        self.client
119            .delete(&self.url(path))
120            .send()
121            .await
122            .expect("Failed to send DELETE request")
123    }
124
125    /// GET request with auth header
126    pub async fn get_auth(&self, path: &str, token: &str) -> reqwest::Response {
127        self.client
128            .get(&self.url(path))
129            .bearer_auth(token)
130            .send()
131            .await
132            .expect("Failed to send authenticated GET request")
133    }
134}