/* * This file is part of Sharenet. * * Sharenet is licensed under the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. * * You may obtain a copy of the license at: * https://creativecommons.org/licenses/by-nc-sa/4.0/ * * Copyright (c) 2024 Continuist */ use axum::{ body::Body, http::{Request, StatusCode}, Json, Router, }; use domain::{User, Product, CreateUser, UpdateUser, CreateProduct, UpdateProduct}; use postgres::{PostgresUserRepository, PostgresProductRepository}; use application::Service; use std::sync::Arc; use tower::ServiceExt; use tower_http::trace::TraceLayer; use tower_http::cors::{CorsLayer, Any}; use uuid::Uuid; use serde_json::json; use axum::response::IntoResponse; use serial_test::serial; use application::UseCase; // Import the centralized test setup use crate::test_setup::{setup_test_db, unique_test_data}; /// Application state containing user and product services. pub struct AppState { user_service: Arc, product_service: Arc

, } impl Clone for AppState where U: Clone, P: Clone, { fn clone(&self) -> Self { Self { user_service: self.user_service.clone(), product_service: self.product_service.clone(), } } } // Helper functions - now using centralized setup // These functions are now imported from test_setup module above pub async fn create_test_app() -> Router { let pool = setup_test_db().await; let user_repo = PostgresUserRepository::new(pool.clone()); let product_repo = PostgresProductRepository::new(pool.clone()); let user_service = Service::new(user_repo); let product_service = Service::new(product_repo); let state = AppState { user_service: Arc::new(user_service), product_service: Arc::new(product_service), }; let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any); Router::new() .route("/users", axum::routing::post(create_user)) .route("/users/:id", axum::routing::get(get_user)) .route("/users", axum::routing::get(list_users)) .route("/users/:id", axum::routing::put(update_user)) .route("/users/:id", axum::routing::delete(delete_user)) .route("/products", axum::routing::post(create_product)) .route("/products/:id", axum::routing::get(get_product)) .route("/products", axum::routing::get(list_products)) .route("/products/:id", axum::routing::put(update_product)) .route("/products/:id", axum::routing::delete(delete_product)) .layer(cors) .layer(TraceLayer::new_for_http()) .with_state(state) } pub async fn extract_json(response: axum::response::Response) -> T { let bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); serde_json::from_slice(&bytes).unwrap() } // Route handler functions async fn create_user( axum::extract::State(state): axum::extract::State, Service>>, Json(data): Json, ) -> impl axum::response::IntoResponse { match state.user_service.create(data).await { Ok(user) => (StatusCode::CREATED, Json(user)).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn get_user( axum::extract::State(state): axum::extract::State, Service>>, axum::extract::Path(id): axum::extract::Path, ) -> impl axum::response::IntoResponse { match state.user_service.get(id).await { Ok(user) => (StatusCode::OK, Json(user)).into_response(), Err(e) => (StatusCode::NOT_FOUND, e.to_string()).into_response(), } } async fn list_users( axum::extract::State(state): axum::extract::State, Service>>, ) -> impl axum::response::IntoResponse { match state.user_service.list().await { Ok(users) => (StatusCode::OK, Json(users)).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn update_user( axum::extract::State(state): axum::extract::State, Service>>, axum::extract::Path(id): axum::extract::Path, Json(data): Json, ) -> impl axum::response::IntoResponse { match state.user_service.update(id, data).await { Ok(user) => (StatusCode::OK, Json(user)).into_response(), Err(e) => (StatusCode::NOT_FOUND, e.to_string()).into_response(), } } async fn delete_user( axum::extract::State(state): axum::extract::State, Service>>, axum::extract::Path(id): axum::extract::Path, ) -> impl axum::response::IntoResponse { match state.user_service.delete(id).await { Ok(_) => StatusCode::NO_CONTENT.into_response(), Err(e) => (StatusCode::NOT_FOUND, e.to_string()).into_response(), } } async fn create_product( axum::extract::State(state): axum::extract::State, Service>>, Json(data): Json, ) -> impl axum::response::IntoResponse { match state.product_service.create(data).await { Ok(product) => (StatusCode::CREATED, Json(product)).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn get_product( axum::extract::State(state): axum::extract::State, Service>>, axum::extract::Path(id): axum::extract::Path, ) -> impl axum::response::IntoResponse { match state.product_service.get(id).await { Ok(product) => (StatusCode::OK, Json(product)).into_response(), Err(e) => (StatusCode::NOT_FOUND, e.to_string()).into_response(), } } async fn list_products( axum::extract::State(state): axum::extract::State, Service>>, ) -> impl axum::response::IntoResponse { match state.product_service.list().await { Ok(products) => (StatusCode::OK, Json(products)).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn update_product( axum::extract::State(state): axum::extract::State, Service>>, axum::extract::Path(id): axum::extract::Path, Json(data): Json, ) -> impl axum::response::IntoResponse { match state.product_service.update(id, data).await { Ok(product) => (StatusCode::OK, Json(product)).into_response(), Err(e) => (StatusCode::NOT_FOUND, e.to_string()).into_response(), } } async fn delete_product( axum::extract::State(state): axum::extract::State, Service>>, axum::extract::Path(id): axum::extract::Path, ) -> impl axum::response::IntoResponse { match state.product_service.delete(id).await { Ok(_) => StatusCode::NO_CONTENT.into_response(), Err(e) => (StatusCode::NOT_FOUND, e.to_string()).into_response(), } } #[tokio::test] #[serial] async fn test_api_with_postgres_user_lifecycle() { let app = create_test_app().await; let (username, email) = unique_test_data("api_user"); // Test user creation via API let create_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/users") .header("content-type", "application/json") .body(Body::from( json!({ "username": username, "email": email }) .to_string(), )) .unwrap(), ) .await .unwrap(); assert_eq!(create_response.status(), StatusCode::CREATED); let user: User = extract_json(create_response).await; assert_eq!(user.username(), username); assert_eq!(user.email(), email); // Test user retrieval via API let get_response = app .clone() .oneshot( Request::builder() .method("GET") .uri(format!("/users/{}", user.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(get_response.status(), StatusCode::OK); let retrieved_user: User = extract_json(get_response).await; assert_eq!(retrieved_user.id(), user.id()); assert_eq!(retrieved_user.username(), username); // Test user update via API let new_username = format!("{}_updated", username); let update_response = app .clone() .oneshot( Request::builder() .method("PUT") .uri(format!("/users/{}", user.id())) .header("content-type", "application/json") .body(Body::from( json!({ "username": new_username, "email": email }) .to_string(), )) .unwrap(), ) .await .unwrap(); assert_eq!(update_response.status(), StatusCode::OK); let updated_user: User = extract_json(update_response).await; assert_eq!(updated_user.username(), new_username); // Test user deletion via API let delete_response = app .clone() .oneshot( Request::builder() .method("DELETE") .uri(format!("/users/{}", user.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(delete_response.status(), StatusCode::NO_CONTENT); // Verify user no longer exists let get_deleted_response = app .clone() .oneshot( Request::builder() .method("GET") .uri(format!("/users/{}", user.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(get_deleted_response.status(), StatusCode::NOT_FOUND); } #[tokio::test] #[serial] async fn test_api_with_postgres_product_lifecycle() { let app = create_test_app().await; let (name, _) = unique_test_data("api_product"); let description = "Test product description"; // Test product creation via API let create_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/products") .header("content-type", "application/json") .body(Body::from( json!({ "name": name, "description": description }) .to_string(), )) .unwrap(), ) .await .unwrap(); assert_eq!(create_response.status(), StatusCode::CREATED); let product: Product = extract_json(create_response).await; assert_eq!(product.name(), name); assert_eq!(product.description(), description); // Test product retrieval via API let get_response = app .clone() .oneshot( Request::builder() .method("GET") .uri(format!("/products/{}", product.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(get_response.status(), StatusCode::OK); let retrieved_product: Product = extract_json(get_response).await; assert_eq!(retrieved_product.id(), product.id()); assert_eq!(retrieved_product.name(), name); // Test product update via API let new_name = format!("{}_updated", name); let update_response = app .clone() .oneshot( Request::builder() .method("PUT") .uri(format!("/products/{}", product.id())) .header("content-type", "application/json") .body(Body::from( json!({ "name": new_name, "description": description }) .to_string(), )) .unwrap(), ) .await .unwrap(); assert_eq!(update_response.status(), StatusCode::OK); let updated_product: Product = extract_json(update_response).await; assert_eq!(updated_product.name(), new_name); // Test product deletion via API let delete_response = app .clone() .oneshot( Request::builder() .method("DELETE") .uri(format!("/products/{}", product.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(delete_response.status(), StatusCode::NO_CONTENT); // Verify product no longer exists let get_deleted_response = app .clone() .oneshot( Request::builder() .method("GET") .uri(format!("/products/{}", product.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(get_deleted_response.status(), StatusCode::NOT_FOUND); } #[tokio::test] #[serial] async fn test_api_with_postgres_list_operations() { let app = create_test_app().await; // Create multiple users let (username1, email1) = unique_test_data("list_user1"); let (username2, email2) = unique_test_data("list_user2"); let user1_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/users") .header("content-type", "application/json") .body(Body::from( json!({ "username": username1, "email": email1 }) .to_string(), )) .unwrap(), ) .await .unwrap(); let user2_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/users") .header("content-type", "application/json") .body(Body::from( json!({ "username": username2, "email": email2 }) .to_string(), )) .unwrap(), ) .await .unwrap(); assert_eq!(user1_response.status(), StatusCode::CREATED); assert_eq!(user2_response.status(), StatusCode::CREATED); // Test list users via API let list_response = app .clone() .oneshot( Request::builder() .method("GET") .uri("/users") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(list_response.status(), StatusCode::OK); let users: Vec = extract_json(list_response).await; assert_eq!(users.len(), 2); let usernames: Vec = users.iter().map(|u| u.username().to_string()).collect(); assert!(usernames.contains(&username1)); assert!(usernames.contains(&username2)); // Create multiple products let (name1, _) = unique_test_data("list_product1"); let (name2, _) = unique_test_data("list_product2"); let product1_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/products") .header("content-type", "application/json") .body(Body::from( json!({ "name": name1, "description": "Description 1" }) .to_string(), )) .unwrap(), ) .await .unwrap(); let product2_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/products") .header("content-type", "application/json") .body(Body::from( json!({ "name": name2, "description": "Description 2" }) .to_string(), )) .unwrap(), ) .await .unwrap(); assert_eq!(product1_response.status(), StatusCode::CREATED); assert_eq!(product2_response.status(), StatusCode::CREATED); // Test list products via API let list_response = app .clone() .oneshot( Request::builder() .method("GET") .uri("/products") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(list_response.status(), StatusCode::OK); let products: Vec = extract_json(list_response).await; assert_eq!(products.len(), 2); let names: Vec = products.iter().map(|p| p.name().to_string()).collect(); assert!(names.contains(&name1)); assert!(names.contains(&name2)); } #[tokio::test] #[serial] async fn test_api_with_postgres_error_handling() { let app = create_test_app().await; // Test getting non-existent user let get_response = app .clone() .oneshot( Request::builder() .method("GET") .uri(format!("/users/{}", Uuid::new_v4())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(get_response.status(), StatusCode::NOT_FOUND); // Test getting non-existent product let get_response = app .clone() .oneshot( Request::builder() .method("GET") .uri(format!("/products/{}", Uuid::new_v4())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(get_response.status(), StatusCode::NOT_FOUND); // Test creating user with duplicate username let (username, email1) = unique_test_data("duplicate_user"); let create1_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/users") .header("content-type", "application/json") .body(Body::from( json!({ "username": username, "email": email1 }) .to_string(), )) .unwrap(), ) .await .unwrap(); assert_eq!(create1_response.status(), StatusCode::CREATED); let email2 = format!("{}_2@example.com", username); let create2_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/users") .header("content-type", "application/json") .body(Body::from( json!({ "username": username, "email": email2 }) .to_string(), )) .unwrap(), ) .await .unwrap(); assert_eq!(create2_response.status(), StatusCode::INTERNAL_SERVER_ERROR); }