Skip to main content

nidus_testing/
app.rs

1use axum::{Extension, Router};
2use http::Method;
3use nidus_config::Config;
4use nidus_core::{
5    Container, LifecycleHook, LifecycleRunner, Module, ModuleDefinition, Nidus, RequestScope,
6    Result,
7};
8use nidus_http::middleware::request_scope_layer;
9use std::sync::Arc;
10
11use crate::request::TestRequest;
12
13/// In-memory test application backed by an Axum router.
14///
15/// `TestApp` drives requests through the router with Tower's in-memory service
16/// path, so no TCP listener is started. Use it for handler, middleware, module,
17/// and provider integration tests.
18///
19/// ```ignore
20/// use axum::{Json, Router, routing::get};
21/// use http::StatusCode;
22/// use nidus_testing::TestApp;
23/// use serde_json::json;
24///
25/// async fn health() -> Json<serde_json::Value> {
26///     Json(json!({ "ok": true }))
27/// }
28///
29/// #[tokio::test]
30/// async fn health_returns_json() {
31///     let app = TestApp::from_router(
32///         Router::new().route("/health", get(health)),
33///     );
34///
35///     let response = app.get("/health").send().await;
36///     response.assert_status(StatusCode::OK);
37///     response.assert_json(json!({ "ok": true }));
38/// }
39/// ```
40#[derive(Clone)]
41pub struct TestApp {
42    router: Router,
43    container: Arc<Container>,
44    config: Config,
45    lifecycle: Arc<LifecycleRunner>,
46}
47
48impl TestApp {
49    /// Creates a test application builder after validating a root Nidus module.
50    pub fn bootstrap<M>() -> Result<TestAppBuilder>
51    where
52        M: Module,
53    {
54        Self::bootstrap_with_router::<M>(Router::new())
55    }
56
57    /// Creates a test application builder with a router after validating a root Nidus module.
58    pub fn bootstrap_with_router<M>(router: Router) -> Result<TestAppBuilder>
59    where
60        M: Module,
61    {
62        Nidus::bootstrap::<M>()?;
63        Ok(Self::builder(router))
64    }
65
66    /// Creates a test application builder after validating an explicit module graph.
67    pub fn bootstrap_with_modules<M, I>(modules: I) -> Result<TestAppBuilder>
68    where
69        M: Module,
70        I: IntoIterator<Item = ModuleDefinition>,
71    {
72        Self::bootstrap_with_modules_and_router::<M, I>(modules, Router::new())
73    }
74
75    /// Creates a test application builder with a router after validating an explicit module graph.
76    pub fn bootstrap_with_modules_and_router<M, I>(
77        modules: I,
78        router: Router,
79    ) -> Result<TestAppBuilder>
80    where
81        M: Module,
82        I: IntoIterator<Item = ModuleDefinition>,
83    {
84        Nidus::bootstrap_with_modules::<M, I>(modules)?;
85        Ok(Self::builder(router))
86    }
87
88    /// Creates a test application from an Axum router.
89    ///
90    /// This is the shortest path for HTTP-only tests. It installs an empty
91    /// Nidus container extension so handlers that need the container can still
92    /// extract it.
93    pub fn from_router(router: Router) -> Self {
94        let container = Arc::new(Container::new());
95        Self {
96            router: router.layer(Extension(Arc::clone(&container))),
97            container,
98            config: Config::new(),
99            lifecycle: Arc::new(LifecycleRunner::new()),
100        }
101    }
102
103    /// Creates a configurable test application builder.
104    pub fn builder(router: Router) -> TestAppBuilder {
105        TestAppBuilder {
106            router,
107            container: Container::new(),
108            config: Config::new(),
109            lifecycle: LifecycleRunner::new(),
110            request_scope: false,
111        }
112    }
113
114    /// Starts a GET request.
115    pub fn get(&self, path: impl Into<String>) -> TestRequest {
116        self.request(Method::GET, path)
117    }
118
119    /// Starts a POST request.
120    pub fn post(&self, path: impl Into<String>) -> TestRequest {
121        self.request(Method::POST, path)
122    }
123
124    /// Starts a PUT request.
125    pub fn put(&self, path: impl Into<String>) -> TestRequest {
126        self.request(Method::PUT, path)
127    }
128
129    /// Starts a PATCH request.
130    pub fn patch(&self, path: impl Into<String>) -> TestRequest {
131        self.request(Method::PATCH, path)
132    }
133
134    /// Starts a DELETE request.
135    pub fn delete(&self, path: impl Into<String>) -> TestRequest {
136        self.request(Method::DELETE, path)
137    }
138
139    /// Starts a request with an arbitrary HTTP method.
140    ///
141    /// The returned [`TestRequest`] can set headers, query parameters, and body
142    /// content before [`TestRequest::send`] executes it against the in-memory
143    /// router.
144    pub fn request(&self, method: Method, path: impl Into<String>) -> TestRequest {
145        TestRequest::new(self.router.clone(), method, path.into())
146    }
147
148    /// Resolves a provider from the test container.
149    pub fn resolve<T>(&self) -> Result<Arc<T>>
150    where
151        T: Send + Sync + 'static,
152    {
153        self.container.resolve::<T>()
154    }
155
156    /// Creates a request scope for resolving request-lifetime providers in tests.
157    pub fn request_scope(&self) -> RequestScope<'_> {
158        self.container.request_scope()
159    }
160
161    /// Returns test configuration overrides.
162    pub fn config(&self) -> &Config {
163        &self.config
164    }
165
166    /// Runs registered test shutdown lifecycle hooks.
167    pub async fn shutdown(&self) -> Result<()> {
168        self.lifecycle.shutdown().await
169    }
170}
171
172/// Builder for in-memory test applications.
173///
174/// Use the builder when tests need provider overrides, request-scoped
175/// providers, config overrides, or lifecycle hooks in addition to an Axum
176/// router.
177pub struct TestAppBuilder {
178    router: Router,
179    container: Container,
180    config: Config,
181    lifecycle: LifecycleRunner,
182    request_scope: bool,
183}
184
185impl TestAppBuilder {
186    /// Registers a provider in the test container.
187    pub fn provider<T>(mut self, value: T) -> Result<Self>
188    where
189        T: Send + Sync + 'static,
190    {
191        self.container.register_singleton(value)?;
192        Ok(self)
193    }
194
195    /// Registers a transient provider factory in the test container.
196    pub fn transient_provider<T, F>(mut self, factory: F) -> Result<Self>
197    where
198        T: Send + Sync + 'static,
199        F: Fn(&Container) -> Result<T> + Send + Sync + 'static,
200    {
201        self.container.register_transient::<T, F>(factory)?;
202        Ok(self)
203    }
204
205    /// Registers a request-lifetime provider factory in the test container.
206    pub fn request_provider<T, F>(mut self, factory: F) -> Result<Self>
207    where
208        T: Send + Sync + 'static,
209        F: Fn(&Container) -> Result<T> + Send + Sync + 'static,
210    {
211        self.container.register_request::<T, F>(factory)?;
212        Ok(self)
213    }
214
215    /// Registers a request-lifetime provider factory that resolves dependencies
216    /// through the active request scope.
217    ///
218    /// This matches production request-scoped resolution. To exercise
219    /// `RequestScoped<T>` extractors over HTTP in the test app, also call
220    /// [`TestAppBuilder::with_request_scope`] so the request scope layer is
221    /// installed on the router.
222    pub fn request_scoped_provider<T, F>(mut self, factory: F) -> Result<Self>
223    where
224        T: Send + Sync + 'static,
225        F: for<'scope> Fn(&RequestScope<'scope>) -> Result<T> + Send + Sync + 'static,
226    {
227        self.container.register_request_scoped::<T, F>(factory)?;
228        Ok(self)
229    }
230
231    /// Installs the production request scope layer so `RequestScoped<T>`
232    /// extractors resolve during HTTP integration tests.
233    ///
234    /// Without this, handlers that extract `RequestScoped<T>` reject with
235    /// `500`/`request_scope_unavailable`. Register request providers with
236    /// [`Self::request_provider`] or [`Self::request_scoped_provider`] first.
237    pub fn with_request_scope(mut self) -> Self {
238        self.request_scope = true;
239        self
240    }
241
242    /// Overrides a provider in the test container.
243    pub fn override_provider<T>(mut self, value: T) -> Result<Self>
244    where
245        T: Send + Sync + 'static,
246    {
247        self.container.override_singleton(value)?;
248        Ok(self)
249    }
250
251    /// Sets configuration overrides for the test application.
252    pub fn config(mut self, config: Config) -> Self {
253        self.config = config;
254        self
255    }
256
257    /// Registers a lifecycle hook for the test application.
258    pub fn lifecycle_hook<H>(mut self, hook: H) -> Self
259    where
260        H: LifecycleHook,
261    {
262        self.lifecycle = self.lifecycle.hook(hook);
263        self
264    }
265
266    /// Builds the test application.
267    pub fn build(self) -> TestApp {
268        let container = Arc::new(self.container);
269        let mut router = self.router.layer(Extension(Arc::clone(&container)));
270        if self.request_scope {
271            router = router.layer(request_scope_layer(Arc::clone(&container)));
272        }
273        TestApp {
274            router,
275            container,
276            config: self.config,
277            lifecycle: Arc::new(self.lifecycle),
278        }
279    }
280
281    /// Runs startup hooks and builds the test application.
282    pub async fn build_started(self) -> Result<TestApp> {
283        self.lifecycle.startup().await?;
284        Ok(self.build())
285    }
286}