use armature_core::{Application, Container, Module, Provider, Router};
use std::sync::Arc;
pub struct TestApp {
pub app: Application,
pub container: Arc<Container>,
}
impl TestApp {
pub fn new(container: Container, router: Router) -> Self {
let app = Application::new(container.clone(), router);
Self {
app,
container: Arc::new(container),
}
}
pub fn get<T: Provider + Clone + 'static>(&self) -> Option<T> {
self.container.get::<T>().ok().map(|arc| (*arc).clone())
}
pub fn client(&self) -> crate::TestClient {
crate::TestClient::new(self.app.router.clone())
}
}
pub struct TestAppBuilder {
container: Container,
router: Router,
}
const WIRE_MODULE_MAX_DEPTH: usize = 64;
impl TestAppBuilder {
pub fn new() -> Self {
Self {
container: Container::new(),
router: Router::new(),
}
}
pub fn register<T: Provider + Clone + 'static>(self, provider: T) -> Self {
self.container.register(provider);
self
}
pub fn add_module<M: Module>(mut self, module: M) -> Self {
let mut visited = std::collections::HashSet::new();
Self::wire_module(&self.container, &mut self.router, &mut visited, &module, 0);
self
}
fn wire_module(
container: &Container,
router: &mut Router,
visited: &mut std::collections::HashSet<std::any::TypeId>,
module: &dyn Module,
depth: usize,
) {
if depth >= WIRE_MODULE_MAX_DEPTH {
panic!("module import graph exceeds depth {WIRE_MODULE_MAX_DEPTH} — cyclic imports?");
}
if !visited.insert(module.module_type_id()) {
return;
}
for imported in module.imports() {
Self::wire_module(container, router, visited, imported.as_ref(), depth + 1);
}
for re_exported in module.re_exports() {
Self::wire_module(container, router, visited, re_exported.as_ref(), depth + 1);
}
for provider_reg in module.providers() {
(provider_reg.register_fn)(container);
}
for controller_reg in module.controllers() {
let instance = (controller_reg.factory)(container).unwrap_or_else(|e| {
panic!(
"TestAppBuilder::add_module: failed to instantiate controller `{}`: {}",
controller_reg.type_name, e
)
});
(controller_reg.route_registrar)(container, router, instance).unwrap_or_else(|e| {
panic!(
"TestAppBuilder::add_module: failed to register routes for controller `{}`: {}",
controller_reg.type_name, e
)
});
}
}
pub fn with_container(self, container: Container) -> Self {
Self { container, ..self }
}
pub fn with_router(self, router: Router) -> Self {
Self { router, ..self }
}
pub fn with_route<F, Fut>(mut self, path: &str, handler: F) -> Self
where
F: Fn(armature_core::HttpRequest) -> Fut + Send + Sync + Clone + 'static,
Fut: std::future::Future<Output = Result<armature_core::HttpResponse, armature_core::Error>>
+ Send
+ 'static,
{
use armature_core::{HttpMethod, Route};
self.router
.add_route(Route::new(HttpMethod::GET, path, handler));
self
}
pub fn build(self) -> TestApp {
TestApp::new(self.container, self.router)
}
}
impl Default for TestAppBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_creation() {
let _builder = TestAppBuilder::new();
}
#[test]
fn test_app_creation() {
let builder = TestAppBuilder::new();
let _app = builder.build();
}
#[derive(Clone)]
struct Widget {
value: u32,
}
#[test]
fn registered_provider_is_retrievable_via_test_app_get() {
let app = TestAppBuilder::new().register(Widget { value: 7 }).build();
let widget = app.get::<Widget>().expect("Widget should be registered");
assert_eq!(widget.value, 7);
}
#[test]
fn unregistered_provider_returns_none() {
let app = TestAppBuilder::new().build();
assert!(app.get::<Widget>().is_none());
}
#[test]
fn with_container_flows_into_test_app() {
let container = Container::new();
container.register(Widget { value: 99 });
let app = TestAppBuilder::new().with_container(container).build();
let widget = app.get::<Widget>().expect("Widget should be registered");
assert_eq!(widget.value, 99);
}
use armature_core::{
ControllerRegistration, Error, HttpMethod, HttpRequest, HttpResponse, ProviderRegistration,
};
use std::any::{Any, TypeId};
#[derive(Clone)]
struct Counter {
value: u32,
}
fn register_counter(container: &Container) {
container.register(Counter { value: 42 });
}
struct CounterController {
count: Arc<Counter>,
}
fn counter_factory(container: &Container) -> Result<Box<dyn Any + Send + Sync>, Error> {
let count = container.resolve::<Counter>()?;
Ok(Box::new(CounterController { count }))
}
fn counter_route_registrar(
_container: &Container,
router: &mut Router,
instance: Box<dyn Any + Send + Sync>,
) -> Result<(), Error> {
let controller = instance
.downcast::<CounterController>()
.expect("instance should be CounterController");
let count = controller.count.clone();
router.add_route(armature_core::Route::new(
HttpMethod::GET,
"/counter",
move |_req: HttpRequest| {
let count = count.clone();
async move { Ok(HttpResponse::ok().with_body(count.value.to_string().into_bytes())) }
},
));
Ok(())
}
struct CounterModule;
impl Module for CounterModule {
fn providers(&self) -> Vec<ProviderRegistration> {
vec![ProviderRegistration {
type_id: TypeId::of::<Counter>(),
type_name: "Counter",
register_fn: register_counter,
}]
}
fn controllers(&self) -> Vec<ControllerRegistration> {
vec![ControllerRegistration {
type_id: TypeId::of::<CounterController>(),
type_name: "CounterController",
base_path: "/counter",
factory: counter_factory,
route_registrar: counter_route_registrar,
}]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![]
}
fn exports(&self) -> Vec<TypeId> {
vec![]
}
}
#[tokio::test]
async fn add_module_registers_providers_and_controllers_for_real() {
let app = TestAppBuilder::new().add_module(CounterModule).build();
let counter = app
.get::<Counter>()
.expect("Counter should be registered by the module");
assert_eq!(counter.value, 42);
let response = app.client().get("/counter").await;
assert_eq!(response.status(), Some(200));
assert_eq!(response.body_string(), Some("42".to_string()));
}
#[derive(Clone)]
struct Pinger;
fn register_pinger(container: &Container) {
container.register(Pinger);
}
struct PingController;
fn ping_factory(container: &Container) -> Result<Box<dyn Any + Send + Sync>, Error> {
container.resolve::<Pinger>()?;
Ok(Box::new(PingController))
}
fn ping_route_registrar(
_container: &Container,
router: &mut Router,
instance: Box<dyn Any + Send + Sync>,
) -> Result<(), Error> {
instance
.downcast::<PingController>()
.expect("instance should be PingController");
router.add_route(armature_core::Route::new(
HttpMethod::GET,
"/ping",
|_req: HttpRequest| async { Ok(HttpResponse::ok().with_body(b"pong".to_vec())) },
));
Ok(())
}
struct PingModule;
impl Module for PingModule {
fn providers(&self) -> Vec<ProviderRegistration> {
vec![ProviderRegistration {
type_id: TypeId::of::<Pinger>(),
type_name: "Pinger",
register_fn: register_pinger,
}]
}
fn controllers(&self) -> Vec<ControllerRegistration> {
vec![ControllerRegistration {
type_id: TypeId::of::<PingController>(),
type_name: "PingController",
base_path: "/ping",
factory: ping_factory,
route_registrar: ping_route_registrar,
}]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![]
}
fn exports(&self) -> Vec<TypeId> {
vec![]
}
}
struct ParentModule;
impl Module for ParentModule {
fn providers(&self) -> Vec<ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![Box::new(CounterModule), Box::new(PingModule)]
}
fn exports(&self) -> Vec<TypeId> {
vec![]
}
}
#[tokio::test]
async fn add_module_registers_all_distinct_imported_modules() {
let app = TestAppBuilder::new().add_module(ParentModule).build();
assert!(app.get::<Counter>().is_some());
assert!(app.get::<Pinger>().is_some());
let counter_response = app.client().get("/counter").await;
assert_eq!(counter_response.status(), Some(200));
assert_eq!(counter_response.body_string(), Some("42".to_string()));
let ping_response = app.client().get("/ping").await;
assert_eq!(ping_response.status(), Some(200));
assert_eq!(ping_response.body_string(), Some("pong".to_string()));
}
static SHARED_REGISTRATIONS: std::sync::atomic::AtomicU32 =
std::sync::atomic::AtomicU32::new(0);
#[derive(Clone)]
struct Shared;
fn register_shared(container: &Container) {
SHARED_REGISTRATIONS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
container.register(Shared);
}
struct SharedModule;
impl Module for SharedModule {
fn providers(&self) -> Vec<ProviderRegistration> {
vec![ProviderRegistration {
type_id: TypeId::of::<Shared>(),
type_name: "Shared",
register_fn: register_shared,
}]
}
fn controllers(&self) -> Vec<ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![]
}
fn exports(&self) -> Vec<TypeId> {
vec![]
}
}
struct DiamondLeftModule;
impl Module for DiamondLeftModule {
fn providers(&self) -> Vec<ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![Box::new(SharedModule)]
}
fn exports(&self) -> Vec<TypeId> {
vec![]
}
}
struct DiamondRightModule;
impl Module for DiamondRightModule {
fn providers(&self) -> Vec<ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![Box::new(SharedModule)]
}
fn exports(&self) -> Vec<TypeId> {
vec![]
}
}
struct DiamondRootModule;
impl Module for DiamondRootModule {
fn providers(&self) -> Vec<ProviderRegistration> {
vec![]
}
fn controllers(&self) -> Vec<ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![Box::new(DiamondLeftModule), Box::new(DiamondRightModule)]
}
fn exports(&self) -> Vec<TypeId> {
vec![]
}
}
#[test]
fn add_module_dedups_diamond_imported_module_registers_shared_provider_once() {
let app = TestAppBuilder::new().add_module(DiamondRootModule).build();
assert_eq!(
SHARED_REGISTRATIONS.load(std::sync::atomic::Ordering::SeqCst),
1,
"SharedModule is reachable via both DiamondLeftModule and \
DiamondRightModule; its provider must register exactly once"
);
assert!(app.get::<Shared>().is_some());
}
static SELF_IMPORT_REGISTRATIONS: std::sync::atomic::AtomicU32 =
std::sync::atomic::AtomicU32::new(0);
#[derive(Clone)]
struct SelfImportMarker;
fn register_self_importing(container: &Container) {
SELF_IMPORT_REGISTRATIONS.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
container.register(SelfImportMarker);
}
struct SelfImportingModule;
impl Module for SelfImportingModule {
fn providers(&self) -> Vec<ProviderRegistration> {
vec![ProviderRegistration {
type_id: TypeId::of::<SelfImportMarker>(),
type_name: "SelfImportMarker",
register_fn: register_self_importing,
}]
}
fn controllers(&self) -> Vec<ControllerRegistration> {
vec![]
}
fn imports(&self) -> Vec<Box<dyn Module>> {
vec![Box::new(SelfImportingModule)]
}
fn exports(&self) -> Vec<TypeId> {
vec![]
}
}
#[test]
fn add_module_dedups_self_importing_module_registers_once_without_panicking() {
let app = TestAppBuilder::new()
.add_module(SelfImportingModule)
.build();
assert_eq!(
SELF_IMPORT_REGISTRATIONS.load(std::sync::atomic::Ordering::SeqCst),
1,
"a self-importing module is visited on its first (root) entry, \
so its own self-import is skipped as already-visited — its \
provider must register exactly once, not zero or many"
);
assert!(app.get::<SelfImportMarker>().is_some());
}
}