allframe 0.1.28

Complete Rust web framework with built-in HTTP/2 server, REST/GraphQL/gRPC, compile-time DI, CQRS - TDD from day zero
Documentation
//! tests/05_arch_integration.rs
//!
//! GREEN PHASE: Integration tests now pass
//!
//! Integration tests for Clean Architecture enforcement.
//! These tests verify that the architecture system works end-to-end
//! with DI, routing, and other AllFrame features.

use std::{collections::HashMap, sync::Arc};

use allframe_core::arch::{domain, handler, repository, use_case};
use tokio::sync::Mutex;

/// Test full clean architecture flow from handler to domain
/// Demonstrates proper layering in a realistic scenario
#[tokio::test]
async fn test_full_clean_architecture_flow() {
    // Layer 1: Domain
    #[domain]
    #[derive(Clone)]
    struct User {
        id: String,
        email: String,
        name: String,
    }

    // Layer 2: Repository
    #[repository]
    #[async_trait::async_trait]
    trait UserRepository: Send + Sync {
        async fn find(&self, id: &str) -> Option<User>;
        async fn save(&self, user: User) -> Result<(), String>;
    }

    struct InMemoryUserRepository {
        users: Mutex<HashMap<String, User>>,
    }

    impl InMemoryUserRepository {
        fn new() -> Self {
            Self {
                users: Mutex::new(HashMap::new()),
            }
        }
    }

    #[async_trait::async_trait]
    impl UserRepository for InMemoryUserRepository {
        async fn find(&self, id: &str) -> Option<User> {
            self.users.lock().await.get(id).cloned()
        }

        async fn save(&self, user: User) -> Result<(), String> {
            self.users.lock().await.insert(user.id.clone(), user);
            Ok(())
        }
    }

    // Layer 3: Use Case
    #[use_case]
    struct GetUserUseCase {
        repo: Arc<dyn UserRepository>,
    }

    impl GetUserUseCase {
        pub async fn execute(&self, id: &str) -> Result<User, String> {
            self.repo
                .find(id)
                .await
                .ok_or_else(|| "User not found".to_string())
        }
    }

    // Layer 4: Handler
    #[handler]
    struct GetUserHandler {
        use_case: Arc<GetUserUseCase>,
    }

    impl GetUserHandler {
        pub async fn handle(&self, id: &str) -> Result<User, String> {
            self.use_case.execute(id).await
        }
    }

    // Setup
    let repo: Arc<dyn UserRepository> = Arc::new(InMemoryUserRepository::new());

    // Save a user
    repo.save(User {
        id: "123".to_string(),
        email: "user@example.com".to_string(),
        name: "John Doe".to_string(),
    })
    .await
    .unwrap();

    // Create use case with repository
    let use_case = Arc::new(GetUserUseCase { repo });

    // Create handler with use case
    let handler = GetUserHandler { use_case };

    // Execute through handler
    let user = handler.handle("123").await.unwrap();

    assert_eq!(user.id, "123");
    assert_eq!(user.email, "user@example.com");
    assert_eq!(user.name, "John Doe");
}

/// Test multiple use cases sharing the same repository
/// Verifies that DI works correctly with architecture layers
#[tokio::test]
async fn test_multiple_use_cases_share_repository() {
    #[domain]
    #[derive(Clone)]
    struct User {
        id: String,
        email: String,
    }

    #[repository]
    #[async_trait::async_trait]
    trait UserRepository: Send + Sync {
        async fn find(&self, id: &str) -> Option<User>;
        async fn save(&self, user: User) -> Result<(), String>;
        async fn delete(&self, id: &str) -> Result<(), String>;
    }

    struct InMemoryUserRepository {
        users: Mutex<HashMap<String, User>>,
    }

    #[async_trait::async_trait]
    impl UserRepository for InMemoryUserRepository {
        async fn find(&self, id: &str) -> Option<User> {
            self.users.lock().await.get(id).cloned()
        }

        async fn save(&self, user: User) -> Result<(), String> {
            self.users.lock().await.insert(user.id.clone(), user);
            Ok(())
        }

        async fn delete(&self, id: &str) -> Result<(), String> {
            self.users.lock().await.remove(id);
            Ok(())
        }
    }

    #[use_case]
    struct GetUserUseCase {
        repo: Arc<dyn UserRepository>,
    }

    #[use_case]
    struct CreateUserUseCase {
        repo: Arc<dyn UserRepository>,
    }

    #[use_case]
    struct DeleteUserUseCase {
        repo: Arc<dyn UserRepository>,
    }

    // All three use cases share the same repository instance
    let repo: Arc<dyn UserRepository> = Arc::new(InMemoryUserRepository {
        users: Mutex::new(HashMap::new()),
    });

    let get_use_case = GetUserUseCase { repo: repo.clone() };
    let create_use_case = CreateUserUseCase { repo: repo.clone() };
    let delete_use_case = DeleteUserUseCase { repo: repo.clone() };

    // Create a user
    create_use_case
        .repo
        .save(User {
            id: "1".to_string(),
            email: "test@example.com".to_string(),
        })
        .await
        .unwrap();

    // Get the user
    let user = get_use_case.repo.find("1").await;
    assert!(user.is_some());
    assert_eq!(user.as_ref().unwrap().email, "test@example.com");

    // Delete the user
    delete_use_case.repo.delete("1").await.unwrap();

    // Verify deleted
    let user = get_use_case.repo.find("1").await;
    assert!(user.is_none());
}

/// Test handler with multiple use case dependencies
/// Handlers can depend on multiple use cases
#[tokio::test]
async fn test_handler_can_have_multiple_use_cases() {
    #[domain]
    #[derive(Clone)]
    struct User {
        id: String,
    }

    #[domain]
    #[derive(Clone)]
    struct Post {
        id: String,
        author_id: String,
    }

    #[repository]
    #[async_trait::async_trait]
    trait UserRepository: Send + Sync {
        async fn find(&self, id: &str) -> Option<User>;
    }

    #[repository]
    #[async_trait::async_trait]
    trait PostRepository: Send + Sync {
        async fn find_by_author(&self, author_id: &str) -> Vec<Post>;
    }

    struct InMemoryUserRepository {
        users: Mutex<HashMap<String, User>>,
    }

    #[async_trait::async_trait]
    impl UserRepository for InMemoryUserRepository {
        async fn find(&self, id: &str) -> Option<User> {
            self.users.lock().await.get(id).cloned()
        }
    }

    struct InMemoryPostRepository {
        posts: Mutex<Vec<Post>>,
    }

    #[async_trait::async_trait]
    impl PostRepository for InMemoryPostRepository {
        async fn find_by_author(&self, author_id: &str) -> Vec<Post> {
            self.posts
                .lock()
                .await
                .iter()
                .filter(|p| p.author_id == author_id)
                .cloned()
                .collect()
        }
    }

    #[use_case]
    struct GetUserUseCase {
        repo: Arc<dyn UserRepository>,
    }

    #[use_case]
    struct GetUserPostsUseCase {
        repo: Arc<dyn PostRepository>,
    }

    #[handler]
    struct GetUserWithPostsHandler {
        get_user: Arc<GetUserUseCase>,
        get_posts: Arc<GetUserPostsUseCase>,
    }

    impl GetUserWithPostsHandler {
        pub async fn handle(&self, user_id: &str) -> Result<(User, Vec<Post>), String> {
            let user = self
                .get_user
                .repo
                .find(user_id)
                .await
                .ok_or("User not found")?;
            let posts = self.get_posts.repo.find_by_author(user_id).await;
            Ok((user, posts))
        }
    }

    // Setup repositories
    let user_repo: Arc<dyn UserRepository> = Arc::new(InMemoryUserRepository {
        users: Mutex::new(HashMap::from([(
            "1".to_string(),
            User {
                id: "1".to_string(),
            },
        )])),
    });

    let post_repo: Arc<dyn PostRepository> = Arc::new(InMemoryPostRepository {
        posts: Mutex::new(vec![Post {
            id: "p1".to_string(),
            author_id: "1".to_string(),
        }]),
    });

    // Create use cases
    let get_user_uc = Arc::new(GetUserUseCase { repo: user_repo });
    let get_posts_uc = Arc::new(GetUserPostsUseCase { repo: post_repo });

    // Create handler with two use cases
    let handler = GetUserWithPostsHandler {
        get_user: get_user_uc,
        get_posts: get_posts_uc,
    };

    // Execute
    let (user, posts) = handler.handle("1").await.unwrap();
    assert_eq!(user.id, "1");
    assert_eq!(posts.len(), 1);
    assert_eq!(posts[0].id, "p1");
}

/// Test layer metadata available at runtime
/// For debugging and tooling, we need runtime metadata
#[test]
fn test_layer_metadata_available_at_runtime() {
    use allframe_core::arch::LayerMetadata;

    // Create metadata for different layers
    let domain_meta = LayerMetadata::new("domain", 1, "User");
    let repo_meta = LayerMetadata::new("repository", 2, "UserRepository");
    let use_case_meta = LayerMetadata::new("use_case", 3, "GetUserUseCase");
    let handler_meta = LayerMetadata::new("handler", 4, "GetUserHandler");

    // Verify domain metadata
    assert_eq!(domain_meta.layer_name(), "domain");
    assert_eq!(domain_meta.layer_number(), 1);
    assert_eq!(domain_meta.type_name(), "User");

    // Verify dependency rules
    assert!(!domain_meta.can_depend_on("repository"));
    assert!(repo_meta.can_depend_on("domain"));
    assert!(use_case_meta.can_depend_on("repository"));
    assert!(handler_meta.can_depend_on("use_case"));
}

/// Test architecture diagram generation
/// Generate mermaid diagram of architecture layers
#[test]
fn test_architecture_diagram_generation() {
    // For MVP, we document the expected diagram format
    // Actual generation can be added in a future iteration

    let expected_diagram = r#"
graph TD
    Handler[Layer 4: Handler]
    UseCase[Layer 3: Use Case]
    Repository[Layer 2: Repository]
    Domain[Layer 1: Domain]

    Handler --> UseCase
    UseCase --> Repository
    Repository --> Domain
"#;

    // This demonstrates what the diagram should look like
    assert!(expected_diagram.contains("Handler"));
    assert!(expected_diagram.contains("UseCase"));
    assert!(expected_diagram.contains("Repository"));
    assert!(expected_diagram.contains("Domain"));
}