1use armature_core::{Application, Container, Module, Provider, Router};
4use std::sync::Arc;
5
6pub struct TestApp {
8 pub app: Application,
9 pub container: Arc<Container>,
10}
11
12impl TestApp {
13 pub fn new(container: Container, router: Router) -> Self {
22 let app = Application::new(container.clone(), router);
23 Self {
24 app,
25 container: Arc::new(container),
26 }
27 }
28
29 pub fn get<T: Provider + Clone + 'static>(&self) -> Option<T> {
35 self.container.get::<T>().ok().map(|arc| (*arc).clone())
36 }
37
38 pub fn client(&self) -> crate::TestClient {
59 crate::TestClient::new(self.app.router.clone())
60 }
61}
62
63pub struct TestAppBuilder {
89 container: Container,
90 router: Router,
91}
92
93const WIRE_MODULE_MAX_DEPTH: usize = 64;
107
108impl TestAppBuilder {
109 pub fn new() -> Self {
111 Self {
112 container: Container::new(),
113 router: Router::new(),
114 }
115 }
116
117 pub fn register<T: Provider + Clone + 'static>(self, provider: T) -> Self {
119 self.container.register(provider);
120 self
121 }
122
123 pub fn add_module<M: Module>(mut self, module: M) -> Self {
155 let mut visited = std::collections::HashSet::new();
156 Self::wire_module(&self.container, &mut self.router, &mut visited, &module, 0);
157 self
158 }
159
160 fn wire_module(
177 container: &Container,
178 router: &mut Router,
179 visited: &mut std::collections::HashSet<std::any::TypeId>,
180 module: &dyn Module,
181 depth: usize,
182 ) {
183 if depth >= WIRE_MODULE_MAX_DEPTH {
184 panic!("module import graph exceeds depth {WIRE_MODULE_MAX_DEPTH} — cyclic imports?");
185 }
186
187 if !visited.insert(module.module_type_id()) {
192 return;
193 }
194
195 for imported in module.imports() {
196 Self::wire_module(container, router, visited, imported.as_ref(), depth + 1);
197 }
198 for re_exported in module.re_exports() {
199 Self::wire_module(container, router, visited, re_exported.as_ref(), depth + 1);
200 }
201
202 for provider_reg in module.providers() {
203 (provider_reg.register_fn)(container);
204 }
205
206 for controller_reg in module.controllers() {
207 let instance = (controller_reg.factory)(container).unwrap_or_else(|e| {
208 panic!(
209 "TestAppBuilder::add_module: failed to instantiate controller `{}`: {}",
210 controller_reg.type_name, e
211 )
212 });
213 (controller_reg.route_registrar)(container, router, instance).unwrap_or_else(|e| {
214 panic!(
215 "TestAppBuilder::add_module: failed to register routes for controller `{}`: {}",
216 controller_reg.type_name, e
217 )
218 });
219 }
220 }
221
222 pub fn with_container(self, container: Container) -> Self {
224 Self { container, ..self }
225 }
226
227 pub fn with_router(self, router: Router) -> Self {
229 Self { router, ..self }
230 }
231
232 pub fn with_route<F, Fut>(mut self, path: &str, handler: F) -> Self
247 where
248 F: Fn(armature_core::HttpRequest) -> Fut + Send + Sync + Clone + 'static,
249 Fut: std::future::Future<Output = Result<armature_core::HttpResponse, armature_core::Error>>
250 + Send
251 + 'static,
252 {
253 use armature_core::{HttpMethod, Route};
254 self.router
256 .add_route(Route::new(HttpMethod::GET, path, handler));
257 self
258 }
259
260 pub fn build(self) -> TestApp {
262 TestApp::new(self.container, self.router)
263 }
264}
265
266impl Default for TestAppBuilder {
267 fn default() -> Self {
268 Self::new()
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_builder_creation() {
278 let _builder = TestAppBuilder::new();
279 }
280
281 #[test]
282 fn test_app_creation() {
283 let builder = TestAppBuilder::new();
284 let _app = builder.build();
285 }
286
287 #[derive(Clone)]
288 struct Widget {
289 value: u32,
290 }
291
292 #[test]
293 fn registered_provider_is_retrievable_via_test_app_get() {
294 let app = TestAppBuilder::new().register(Widget { value: 7 }).build();
295
296 let widget = app.get::<Widget>().expect("Widget should be registered");
297 assert_eq!(widget.value, 7);
298 }
299
300 #[test]
301 fn unregistered_provider_returns_none() {
302 let app = TestAppBuilder::new().build();
303 assert!(app.get::<Widget>().is_none());
304 }
305
306 #[test]
307 fn with_container_flows_into_test_app() {
308 let container = Container::new();
309 container.register(Widget { value: 99 });
310
311 let app = TestAppBuilder::new().with_container(container).build();
312
313 let widget = app.get::<Widget>().expect("Widget should be registered");
314 assert_eq!(widget.value, 99);
315 }
316
317 use armature_core::{
320 ControllerRegistration, Error, HttpMethod, HttpRequest, HttpResponse, ProviderRegistration,
321 };
322 use std::any::{Any, TypeId};
323
324 #[derive(Clone)]
325 struct Counter {
326 value: u32,
327 }
328
329 fn register_counter(container: &Container) {
330 container.register(Counter { value: 42 });
331 }
332
333 struct CounterController {
334 count: Arc<Counter>,
335 }
336
337 fn counter_factory(container: &Container) -> Result<Box<dyn Any + Send + Sync>, Error> {
338 let count = container.resolve::<Counter>()?;
339 Ok(Box::new(CounterController { count }))
340 }
341
342 fn counter_route_registrar(
343 _container: &Container,
344 router: &mut Router,
345 instance: Box<dyn Any + Send + Sync>,
346 ) -> Result<(), Error> {
347 let controller = instance
348 .downcast::<CounterController>()
349 .expect("instance should be CounterController");
350 let count = controller.count.clone();
351 router.add_route(armature_core::Route::new(
352 HttpMethod::GET,
353 "/counter",
354 move |_req: HttpRequest| {
355 let count = count.clone();
356 async move { Ok(HttpResponse::ok().with_body(count.value.to_string().into_bytes())) }
357 },
358 ));
359 Ok(())
360 }
361
362 struct CounterModule;
363 impl Module for CounterModule {
364 fn providers(&self) -> Vec<ProviderRegistration> {
365 vec![ProviderRegistration {
366 type_id: TypeId::of::<Counter>(),
367 type_name: "Counter",
368 register_fn: register_counter,
369 }]
370 }
371 fn controllers(&self) -> Vec<ControllerRegistration> {
372 vec![ControllerRegistration {
373 type_id: TypeId::of::<CounterController>(),
374 type_name: "CounterController",
375 base_path: "/counter",
376 factory: counter_factory,
377 route_registrar: counter_route_registrar,
378 }]
379 }
380 fn imports(&self) -> Vec<Box<dyn Module>> {
381 vec![]
382 }
383 fn exports(&self) -> Vec<TypeId> {
384 vec![]
385 }
386 }
387
388 #[tokio::test]
389 async fn add_module_registers_providers_and_controllers_for_real() {
390 let app = TestAppBuilder::new().add_module(CounterModule).build();
391
392 let counter = app
394 .get::<Counter>()
395 .expect("Counter should be registered by the module");
396 assert_eq!(counter.value, 42);
397
398 let response = app.client().get("/counter").await;
402 assert_eq!(response.status(), Some(200));
403 assert_eq!(response.body_string(), Some("42".to_string()));
404 }
405
406 #[derive(Clone)]
407 struct Pinger;
408
409 fn register_pinger(container: &Container) {
410 container.register(Pinger);
411 }
412
413 struct PingController;
414
415 fn ping_factory(container: &Container) -> Result<Box<dyn Any + Send + Sync>, Error> {
416 container.resolve::<Pinger>()?;
417 Ok(Box::new(PingController))
418 }
419
420 fn ping_route_registrar(
421 _container: &Container,
422 router: &mut Router,
423 instance: Box<dyn Any + Send + Sync>,
424 ) -> Result<(), Error> {
425 instance
426 .downcast::<PingController>()
427 .expect("instance should be PingController");
428 router.add_route(armature_core::Route::new(
429 HttpMethod::GET,
430 "/ping",
431 |_req: HttpRequest| async { Ok(HttpResponse::ok().with_body(b"pong".to_vec())) },
432 ));
433 Ok(())
434 }
435
436 struct PingModule;
437 impl Module for PingModule {
438 fn providers(&self) -> Vec<ProviderRegistration> {
439 vec![ProviderRegistration {
440 type_id: TypeId::of::<Pinger>(),
441 type_name: "Pinger",
442 register_fn: register_pinger,
443 }]
444 }
445 fn controllers(&self) -> Vec<ControllerRegistration> {
446 vec![ControllerRegistration {
447 type_id: TypeId::of::<PingController>(),
448 type_name: "PingController",
449 base_path: "/ping",
450 factory: ping_factory,
451 route_registrar: ping_route_registrar,
452 }]
453 }
454 fn imports(&self) -> Vec<Box<dyn Module>> {
455 vec![]
456 }
457 fn exports(&self) -> Vec<TypeId> {
458 vec![]
459 }
460 }
461
462 struct ParentModule;
470 impl Module for ParentModule {
471 fn providers(&self) -> Vec<ProviderRegistration> {
472 vec![]
473 }
474 fn controllers(&self) -> Vec<ControllerRegistration> {
475 vec![]
476 }
477 fn imports(&self) -> Vec<Box<dyn Module>> {
478 vec![Box::new(CounterModule), Box::new(PingModule)]
479 }
480 fn exports(&self) -> Vec<TypeId> {
481 vec![]
482 }
483 }
484
485 #[tokio::test]
486 async fn add_module_registers_all_distinct_imported_modules() {
487 let app = TestAppBuilder::new().add_module(ParentModule).build();
488
489 assert!(app.get::<Counter>().is_some());
491 assert!(app.get::<Pinger>().is_some());
492
493 let counter_response = app.client().get("/counter").await;
495 assert_eq!(counter_response.status(), Some(200));
496 assert_eq!(counter_response.body_string(), Some("42".to_string()));
497
498 let ping_response = app.client().get("/ping").await;
499 assert_eq!(ping_response.status(), Some(200));
500 assert_eq!(ping_response.body_string(), Some("pong".to_string()));
501 }
502
503 static SHARED_REGISTRATIONS: std::sync::atomic::AtomicU32 =
510 std::sync::atomic::AtomicU32::new(0);
511
512 #[derive(Clone)]
513 struct Shared;
514
515 fn register_shared(container: &Container) {
516 SHARED_REGISTRATIONS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
517 container.register(Shared);
518 }
519
520 struct SharedModule;
521 impl Module for SharedModule {
522 fn providers(&self) -> Vec<ProviderRegistration> {
523 vec![ProviderRegistration {
524 type_id: TypeId::of::<Shared>(),
525 type_name: "Shared",
526 register_fn: register_shared,
527 }]
528 }
529 fn controllers(&self) -> Vec<ControllerRegistration> {
530 vec![]
531 }
532 fn imports(&self) -> Vec<Box<dyn Module>> {
533 vec![]
534 }
535 fn exports(&self) -> Vec<TypeId> {
536 vec![]
537 }
538 }
539
540 struct DiamondLeftModule;
542 impl Module for DiamondLeftModule {
543 fn providers(&self) -> Vec<ProviderRegistration> {
544 vec![]
545 }
546 fn controllers(&self) -> Vec<ControllerRegistration> {
547 vec![]
548 }
549 fn imports(&self) -> Vec<Box<dyn Module>> {
550 vec![Box::new(SharedModule)]
551 }
552 fn exports(&self) -> Vec<TypeId> {
553 vec![]
554 }
555 }
556
557 struct DiamondRightModule;
559 impl Module for DiamondRightModule {
560 fn providers(&self) -> Vec<ProviderRegistration> {
561 vec![]
562 }
563 fn controllers(&self) -> Vec<ControllerRegistration> {
564 vec![]
565 }
566 fn imports(&self) -> Vec<Box<dyn Module>> {
567 vec![Box::new(SharedModule)]
568 }
569 fn exports(&self) -> Vec<TypeId> {
570 vec![]
571 }
572 }
573
574 struct DiamondRootModule;
581 impl Module for DiamondRootModule {
582 fn providers(&self) -> Vec<ProviderRegistration> {
583 vec![]
584 }
585 fn controllers(&self) -> Vec<ControllerRegistration> {
586 vec![]
587 }
588 fn imports(&self) -> Vec<Box<dyn Module>> {
589 vec![Box::new(DiamondLeftModule), Box::new(DiamondRightModule)]
590 }
591 fn exports(&self) -> Vec<TypeId> {
592 vec![]
593 }
594 }
595
596 #[test]
597 fn add_module_dedups_diamond_imported_module_registers_shared_provider_once() {
598 let app = TestAppBuilder::new().add_module(DiamondRootModule).build();
599
600 assert_eq!(
601 SHARED_REGISTRATIONS.load(std::sync::atomic::Ordering::SeqCst),
602 1,
603 "SharedModule is reachable via both DiamondLeftModule and \
604 DiamondRightModule; its provider must register exactly once"
605 );
606 assert!(app.get::<Shared>().is_some());
607 }
608
609 static SELF_IMPORT_REGISTRATIONS: std::sync::atomic::AtomicU32 =
617 std::sync::atomic::AtomicU32::new(0);
618
619 #[derive(Clone)]
620 struct SelfImportMarker;
621
622 fn register_self_importing(container: &Container) {
623 SELF_IMPORT_REGISTRATIONS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
624 container.register(SelfImportMarker);
625 }
626
627 struct SelfImportingModule;
639 impl Module for SelfImportingModule {
640 fn providers(&self) -> Vec<ProviderRegistration> {
641 vec![ProviderRegistration {
642 type_id: TypeId::of::<SelfImportMarker>(),
643 type_name: "SelfImportMarker",
644 register_fn: register_self_importing,
645 }]
646 }
647 fn controllers(&self) -> Vec<ControllerRegistration> {
648 vec![]
649 }
650 fn imports(&self) -> Vec<Box<dyn Module>> {
651 vec![Box::new(SelfImportingModule)]
652 }
653 fn exports(&self) -> Vec<TypeId> {
654 vec![]
655 }
656 }
657
658 #[test]
659 fn add_module_dedups_self_importing_module_registers_once_without_panicking() {
660 let app = TestAppBuilder::new()
661 .add_module(SelfImportingModule)
662 .build();
663
664 assert_eq!(
665 SELF_IMPORT_REGISTRATIONS.load(std::sync::atomic::Ordering::SeqCst),
666 1,
667 "a self-importing module is visited on its first (root) entry, \
668 so its own self-import is skipped as already-visited — its \
669 provider must register exactly once, not zero or many"
670 );
671 assert!(app.get::<SelfImportMarker>().is_some());
672 }
673}