//! # api //! //! This crate provides a RESTful HTTP API for the Sharenet application using Axum. //! It implements CRUD operations for users and products with proper error handling, //! CORS support, and comprehensive testing. //! //! ## Features //! - RESTful HTTP API with Axum framework //! - CRUD operations for `User` and `Product` entities //! - Generic service layer supporting different backends (memory, PostgreSQL) //! - CORS support for cross-origin requests //! - Request/response logging with tracing //! - Comprehensive unit tests with mock services //! - Proper HTTP status codes and error handling //! //! ## API Endpoints //! //! ### Health Check //! - `GET /health` - Health check endpoint for monitoring //! //! ### Users //! - `POST /users` - Create a new user //! - `GET /users/:id` - Get a user by ID //! - `GET /users` - List all users //! - `PUT /users/:id` - Update a user //! - `DELETE /users/:id` - Delete a user //! //! ### Products //! - `POST /products` - Create a new product //! - `GET /products/:id` - Get a product by ID //! - `GET /products` - List all products //! - `PUT /products/:id` - Update a product //! - `DELETE /products/:id` - Delete a product //! //! ## Usage //! //! ```rust //! use api::run; //! use std::net::SocketAddr; //! //! // Example with any service implementing UseCase and UseCase //! #[tokio::main] //! async fn main() { //! let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); //! //! // Create your user and product services here //! // let user_service = YourUserService::new(); //! // let product_service = YourProductService::new(); //! //! // run(addr, user_service, product_service).await; //! } //! ``` //! //! ## Testing //! //! The crate includes comprehensive unit tests using mock services: //! - Tests for all CRUD operations //! - Error handling scenarios (not found, validation errors) //! - Integration tests covering full entity lifecycles //! - Uses `tower::ServiceExt::oneshot()` for testing Axum applications //! //! Run tests with: //! ```bash //! cargo test //! ``` //! //! ## Dependencies //! - `axum` - HTTP web framework //! - `tower` - Middleware framework with `util` feature for testing //! - `tower-http` - HTTP-specific middleware (CORS, tracing) //! - `tracing` - Application logging and observability //! - `serde` - Serialization/deserialization //! - `uuid` - Unique identifier generation /* * 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 std::net::SocketAddr; use std::sync::Arc; use application::UseCase; use axum::{ extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::{delete, get, post, put}, Json, Router, }; use domain::{CreateProduct, CreateUser, Product, UpdateProduct, UpdateUser, User}; use tower_http::trace::TraceLayer; use tower_http::cors::{CorsLayer, Any}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, filter::EnvFilter}; use uuid::Uuid; /// Application state containing user and product services. /// /// This struct holds references to the service implementations that handle /// business logic for users and products. It's generic over the service types /// to allow different implementations (memory, PostgreSQL, etc.). 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(), } } } /// Starts the HTTP server with the provided services. /// /// This function sets up the Axum application with all routes, middleware, /// and state management. It configures CORS, request logging, and starts /// listening on the specified address. /// /// # Arguments /// * `addr` - The socket address to bind the server to /// * `user_service` - Service implementation for user operations /// * `product_service` - Service implementation for product operations /// /// See the module-level documentation for usage examples. pub async fn run(addr: SocketAddr, user_service: U, product_service: P) where U: UseCase + Clone + Send + Sync + 'static, P: UseCase + Clone + Send + Sync + 'static, { tracing_subscriber::registry() .with(EnvFilter::new( std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()), )) .with(tracing_subscriber::fmt::layer()) .init(); let state = AppState { user_service: Arc::new(user_service), product_service: Arc::new(product_service), }; // Configure CORS let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any); let app = Router::new() .route("/health", get(health_check)) .route("/users", post(create_user::)) .route("/users/:id", get(get_user::)) .route("/users", get(list_users::)) .route("/users/:id", put(update_user::)) .route("/users/:id", delete(delete_user::)) .route("/products", post(create_product::

)) .route("/products/:id", get(get_product::

)) .route("/products", get(list_products::

)) .route("/products/:id", put(update_product::

)) .route("/products/:id", delete(delete_product::

)) .layer(cors) .layer(TraceLayer::new_for_http()) .with_state(state); tracing::info!("listening on {}", addr); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::serve(listener, app).await.unwrap(); } /// Creates a new user. /// /// Accepts a JSON payload with `username` and `email` fields. /// Returns the created user with generated ID and timestamps. /// /// # Response /// - `201 Created` - User successfully created /// - `500 Internal Server Error` - Service error async fn create_user( State(state): State>>, Json(data): Json, ) -> impl IntoResponse where U: UseCase, { 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(), } } /// Retrieves a user by ID. /// /// # Response /// - `200 OK` - User found and returned /// - `404 Not Found` - User with specified ID not found async fn get_user( State(state): State>>, Path(id): Path, ) -> impl IntoResponse where U: UseCase, { 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(), } } /// Lists all users. /// /// # Response /// - `200 OK` - List of all users /// - `500 Internal Server Error` - Service error async fn list_users( State(state): State>>, ) -> impl IntoResponse where U: UseCase, { 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(), } } /// Updates a user by ID. /// /// Accepts a JSON payload with optional `username` and `email` fields. /// Only provided fields will be updated. /// /// # Response /// - `200 OK` - User successfully updated /// - `404 Not Found` - User with specified ID not found async fn update_user( State(state): State>>, Path(id): Path, Json(data): Json, ) -> impl IntoResponse where U: UseCase, { 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(), } } /// Deletes a user by ID. /// /// Returns a 204 No Content response on successful deletion. /// /// # Response /// - `204 No Content` - User successfully deleted /// - `404 Not Found` - User with specified ID not found async fn delete_user( State(state): State>>, Path(id): Path, ) -> impl IntoResponse where U: UseCase, { match state.user_service.delete(id).await { Ok(_) => StatusCode::NO_CONTENT.into_response(), Err(e) => (StatusCode::NOT_FOUND, e.to_string()).into_response(), } } /// Creates a new product. /// /// Accepts a JSON payload with `name` and `description` fields. /// Returns the created product with generated ID and timestamps. /// /// # Response /// - `201 Created` - Product successfully created /// - `500 Internal Server Error` - Service error async fn create_product

( State(state): State, P>>, Json(data): Json, ) -> impl IntoResponse where P: UseCase, { 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(), } } /// Retrieves a product by ID. /// /// # Response /// - `200 OK` - Product found and returned /// - `404 Not Found` - Product with specified ID not found async fn get_product

( State(state): State, P>>, Path(id): Path, ) -> impl IntoResponse where P: UseCase, { 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(), } } /// Lists all products. /// /// # Response /// - `200 OK` - List of all products /// - `500 Internal Server Error` - Service error async fn list_products

( State(state): State, P>>, ) -> impl IntoResponse where P: UseCase, { 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(), } } /// Updates a product by ID. /// /// Accepts a JSON payload with optional `name` and `description` fields. /// Only provided fields will be updated. /// /// # Response /// - `200 OK` - Product successfully updated /// - `404 Not Found` - Product with specified ID not found async fn update_product

( State(state): State, P>>, Path(id): Path, Json(data): Json, ) -> impl IntoResponse where P: UseCase, { 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(), } } /// Deletes a product by ID. /// /// Returns a 204 No Content response on successful deletion. /// /// # Response /// - `204 No Content` - Product successfully deleted /// - `404 Not Found` - Product with specified ID not found async fn delete_product

( State(state): State, P>>, Path(id): Path, ) -> impl IntoResponse where P: UseCase, { match state.product_service.delete(id).await { Ok(_) => StatusCode::NO_CONTENT.into_response(), Err(e) => (StatusCode::NOT_FOUND, e.to_string()).into_response(), } } /// Health check endpoint for monitoring and load balancers. /// /// Returns a simple JSON response indicating the service is healthy. /// This endpoint is used by Docker healthchecks and monitoring systems. /// /// # Response /// - `200 OK` - Service is healthy async fn health_check() -> impl IntoResponse { (StatusCode::OK, Json(serde_json::json!({ "status": "healthy", "service": "sharenet-api", "timestamp": chrono::Utc::now().to_rfc3339() }))) } #[cfg(test)] mod tests { //! # API Tests //! //! This module contains comprehensive unit tests for the API endpoints. //! Tests use mock services to verify HTTP request/response behavior //! without requiring external dependencies. //! //! ## Test Structure //! - `user_endpoints` - Tests for user CRUD operations //! - `product_endpoints` - Tests for product CRUD operations //! - `integration_tests` - End-to-end lifecycle tests //! //! ## Testing Approach //! - Uses `tower::ServiceExt::oneshot()` for testing Axum applications //! - Mock services implement the `UseCase` trait //! - Tests verify HTTP status codes, response bodies, and error handling //! - Integration tests cover complete entity lifecycles use super::*; use axum::{ body::Body, http::{Request, StatusCode}, response::Response, }; use domain::{CreateProduct, CreateUser, UpdateProduct, UpdateUser, User, Product, DomainError}; use application::{UseCase, ApplicationError}; use std::sync::Arc; use tokio::sync::RwLock; use std::collections::HashMap; use serde_json::json; use tower::ServiceExt; /// Mock user service for testing. /// /// Implements the `UseCase` trait using an in-memory HashMap /// for storing test data. Provides thread-safe access via `RwLock`. #[derive(Clone)] struct MockUserService { users: Arc>>, } impl MockUserService { /// Creates a new mock user service with empty storage. fn new() -> Self { Self { users: Arc::new(RwLock::new(HashMap::new())), } } } impl UseCase for MockUserService { fn create(&self, data: CreateUser) -> impl std::future::Future> + Send { let users = self.users.clone(); async move { let mut guard = users.write().await; let id = Uuid::new_v4(); let user = User::new(id, data.username().to_string(), data.email().to_string())?; guard.insert(id, user.clone()); Ok(user) } } fn get(&self, id: Uuid) -> impl std::future::Future> + Send { let users = self.users.clone(); async move { let guard = users.read().await; guard.get(&id) .cloned() .ok_or_else(|| ApplicationError::Domain(DomainError::NotFound(format!("User not found: {}", id)))) } } fn list(&self) -> impl std::future::Future, ApplicationError>> + Send { let users = self.users.clone(); async move { let guard = users.read().await; Ok(guard.values().cloned().collect()) } } fn update(&self, id: Uuid, data: UpdateUser) -> impl std::future::Future> + Send { let users = self.users.clone(); async move { let mut guard = users.write().await; let user = guard.get_mut(&id) .ok_or_else(|| ApplicationError::Domain(DomainError::NotFound(format!("User not found: {}", id))))?; if let Some(username) = data.username() { user.set_username(username.to_string())?; } if let Some(email) = data.email() { user.set_email(email.to_string())?; } Ok(user.clone()) } } fn delete(&self, id: Uuid) -> impl std::future::Future> + Send { let users = self.users.clone(); async move { let mut guard = users.write().await; guard.remove(&id) .ok_or_else(|| ApplicationError::Domain(DomainError::NotFound(format!("User not found: {}", id))))?; Ok(()) } } } /// Mock product service for testing. /// /// Implements the `UseCase` trait using an in-memory HashMap /// for storing test data. Provides thread-safe access via `RwLock`. #[derive(Clone)] struct MockProductService { products: Arc>>, } impl MockProductService { /// Creates a new mock product service with empty storage. fn new() -> Self { Self { products: Arc::new(RwLock::new(HashMap::new())), } } } impl UseCase for MockProductService { fn create(&self, data: CreateProduct) -> impl std::future::Future> + Send { let products = self.products.clone(); async move { let mut guard = products.write().await; let id = Uuid::new_v4(); let product = Product::new(id, data.name().to_string(), data.description().to_string())?; guard.insert(id, product.clone()); Ok(product) } } fn get(&self, id: Uuid) -> impl std::future::Future> + Send { let products = self.products.clone(); async move { let guard = products.read().await; guard.get(&id) .cloned() .ok_or_else(|| ApplicationError::Domain(DomainError::NotFound(format!("Product not found: {}", id)))) } } fn list(&self) -> impl std::future::Future, ApplicationError>> + Send { let products = self.products.clone(); async move { let guard = products.read().await; Ok(guard.values().cloned().collect()) } } fn update(&self, id: Uuid, data: UpdateProduct) -> impl std::future::Future> + Send { let products = self.products.clone(); async move { let mut guard = products.write().await; let product = guard.get_mut(&id) .ok_or_else(|| ApplicationError::Domain(DomainError::NotFound(format!("Product not found: {}", id))))?; if let Some(name) = data.name() { product.set_name(name.to_string())?; } if let Some(description) = data.description() { product.set_description(description.to_string())?; } Ok(product.clone()) } } fn delete(&self, id: Uuid) -> impl std::future::Future> + Send { let products = self.products.clone(); async move { let mut guard = products.write().await; guard.remove(&id) .ok_or_else(|| ApplicationError::Domain(DomainError::NotFound(format!("Product not found: {}", id))))?; Ok(()) } } } /// Creates a test Axum application with mock services. /// /// Returns a configured `Router` with all endpoints and mock services /// for testing purposes. fn create_test_app() -> Router { let user_service = MockUserService::new(); let product_service = MockProductService::new(); let state = AppState { user_service: Arc::new(user_service), product_service: Arc::new(product_service), }; Router::new() .route("/health", get(health_check)) .route("/users", post(create_user::)) .route("/users/:id", get(get_user::)) .route("/users", get(list_users::)) .route("/users/:id", put(update_user::)) .route("/users/:id", delete(delete_user::)) .route("/products", post(create_product::)) .route("/products/:id", get(get_product::)) .route("/products", get(list_products::)) .route("/products/:id", put(update_product::)) .route("/products/:id", delete(delete_product::)) .with_state(state) } /// Extracts JSON data from an HTTP response. /// /// Helper function for tests to deserialize response bodies. async fn extract_json(response: Response) -> T { let bytes = axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap(); serde_json::from_slice(&bytes).unwrap() } mod user_endpoints { //! # User Endpoint Tests //! //! Tests for all user-related API endpoints including CRUD operations //! and error handling scenarios. use super::*; /// Tests user creation with valid data. #[tokio::test] async fn test_create_user() { let app = create_test_app(); let create_data = json!({ "username": "testuser", "email": "test@example.com" }); let response = app .oneshot( Request::builder() .method("POST") .uri("/users") .header("content-type", "application/json") .body(Body::from(create_data.to_string())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::CREATED); let user: User = extract_json(response).await; assert_eq!(user.username(), "testuser"); assert_eq!(user.email(), "test@example.com"); assert!(!user.id().is_nil()); } #[tokio::test] async fn test_get_user() { let app = create_test_app(); // First create a user let create_data = json!({ "username": "testuser", "email": "test@example.com" }); let create_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/users") .header("content-type", "application/json") .body(Body::from(create_data.to_string())) .unwrap(), ) .await .unwrap(); let created_user: User = extract_json(create_response).await; // Then get the user let response = app .oneshot( Request::builder() .method("GET") .uri(format!("/users/{}", created_user.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let user: User = extract_json(response).await; assert_eq!(user.id(), created_user.id()); assert_eq!(user.username(), "testuser"); assert_eq!(user.email(), "test@example.com"); } #[tokio::test] async fn test_get_user_not_found() { let app = create_test_app(); let non_existent_id = Uuid::new_v4(); let response = app .oneshot( Request::builder() .method("GET") .uri(format!("/users/{}", non_existent_id)) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_list_users() { let app = create_test_app(); // Create two users let user1_data = json!({ "username": "user1", "email": "user1@example.com" }); let user2_data = json!({ "username": "user2", "email": "user2@example.com" }); app.clone() .oneshot( Request::builder() .method("POST") .uri("/users") .header("content-type", "application/json") .body(Body::from(user1_data.to_string())) .unwrap(), ) .await .unwrap(); app.clone() .oneshot( Request::builder() .method("POST") .uri("/users") .header("content-type", "application/json") .body(Body::from(user2_data.to_string())) .unwrap(), ) .await .unwrap(); // List users let response = app .oneshot( Request::builder() .method("GET") .uri("/users") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let users: Vec = extract_json(response).await; assert_eq!(users.len(), 2); assert!(users.iter().any(|u| u.username() == "user1")); assert!(users.iter().any(|u| u.username() == "user2")); } #[tokio::test] async fn test_update_user() { let app = create_test_app(); // First create a user let create_data = json!({ "username": "olduser", "email": "old@example.com" }); let create_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/users") .header("content-type", "application/json") .body(Body::from(create_data.to_string())) .unwrap(), ) .await .unwrap(); let created_user: User = extract_json(create_response).await; // Update the user let update_data = json!({ "username": "newuser", "email": "new@example.com" }); let response = app .oneshot( Request::builder() .method("PUT") .uri(format!("/users/{}", created_user.id())) .header("content-type", "application/json") .body(Body::from(update_data.to_string())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let user: User = extract_json(response).await; assert_eq!(user.id(), created_user.id()); assert_eq!(user.username(), "newuser"); assert_eq!(user.email(), "new@example.com"); } #[tokio::test] async fn test_update_user_partial() { let app = create_test_app(); // First create a user let create_data = json!({ "username": "testuser", "email": "test@example.com" }); let create_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/users") .header("content-type", "application/json") .body(Body::from(create_data.to_string())) .unwrap(), ) .await .unwrap(); let created_user: User = extract_json(create_response).await; // Update only username let update_data = json!({ "username": "newuser" }); let response = app .oneshot( Request::builder() .method("PUT") .uri(format!("/users/{}", created_user.id())) .header("content-type", "application/json") .body(Body::from(update_data.to_string())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let user: User = extract_json(response).await; assert_eq!(user.id(), created_user.id()); assert_eq!(user.username(), "newuser"); assert_eq!(user.email(), "test@example.com"); // Should remain unchanged } #[tokio::test] async fn test_update_user_not_found() { let app = create_test_app(); let non_existent_id = Uuid::new_v4(); let update_data = json!({ "username": "newuser" }); let response = app .oneshot( Request::builder() .method("PUT") .uri(format!("/users/{}", non_existent_id)) .header("content-type", "application/json") .body(Body::from(update_data.to_string())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_delete_user() { let app = create_test_app(); // First create a user let create_data = json!({ "username": "testuser", "email": "test@example.com" }); let create_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/users") .header("content-type", "application/json") .body(Body::from(create_data.to_string())) .unwrap(), ) .await .unwrap(); let created_user: User = extract_json(create_response).await; // Delete the user let response = app .clone() .oneshot( Request::builder() .method("DELETE") .uri(format!("/users/{}", created_user.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::NO_CONTENT); // Verify user is deleted let get_response = app .oneshot( Request::builder() .method("GET") .uri(format!("/users/{}", created_user.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(get_response.status(), StatusCode::NOT_FOUND); } #[tokio::test] async fn test_delete_user_not_found() { let app = create_test_app(); let non_existent_id = Uuid::new_v4(); let response = app .oneshot( Request::builder() .method("DELETE") .uri(format!("/users/{}", non_existent_id)) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } } mod health_check { //! # Health Check Endpoint Tests //! //! Tests for the health check endpoint used by Docker healthchecks //! and monitoring systems. use super::*; /// Tests the health check endpoint returns a healthy status. #[tokio::test] async fn test_health_check() { let app = create_test_app(); let response = app .oneshot( Request::builder() .method("GET") .uri("/health") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let health_data: serde_json::Value = extract_json(response).await; assert_eq!(health_data["status"], "healthy"); assert_eq!(health_data["service"], "sharenet-api"); assert!(health_data["timestamp"].is_string()); } } mod product_endpoints { //! # Product Endpoint Tests //! //! Tests for all product-related API endpoints including CRUD operations //! and error handling scenarios. use super::*; /// Tests product creation with valid data. #[tokio::test] async fn test_create_product() { let app = create_test_app(); let create_data = json!({ "name": "Test Product", "description": "Test Description" }); let response = app .oneshot( Request::builder() .method("POST") .uri("/products") .header("content-type", "application/json") .body(Body::from(create_data.to_string())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::CREATED); let product: Product = extract_json(response).await; assert_eq!(product.name(), "Test Product"); assert_eq!(product.description(), "Test Description"); assert!(!product.id().is_nil()); } /// Tests retrieving a product by ID. #[tokio::test] async fn test_get_product() { let app = create_test_app(); // First create a product let create_data = json!({ "name": "Test Product", "description": "Test Description" }); let create_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/products") .header("content-type", "application/json") .body(Body::from(create_data.to_string())) .unwrap(), ) .await .unwrap(); let created_product: Product = extract_json(create_response).await; // Then get the product let response = app .oneshot( Request::builder() .method("GET") .uri(format!("/products/{}", created_product.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let product: Product = extract_json(response).await; assert_eq!(product.id(), created_product.id()); assert_eq!(product.name(), "Test Product"); assert_eq!(product.description(), "Test Description"); } /// Tests retrieving a non-existent product. #[tokio::test] async fn test_get_product_not_found() { let app = create_test_app(); let non_existent_id = Uuid::new_v4(); let response = app .oneshot( Request::builder() .method("GET") .uri(format!("/products/{}", non_existent_id)) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } /// Tests listing all products. #[tokio::test] async fn test_list_products() { let app = create_test_app(); // Create two products let product1_data = json!({ "name": "Product 1", "description": "Description 1" }); let product2_data = json!({ "name": "Product 2", "description": "Description 2" }); app.clone() .oneshot( Request::builder() .method("POST") .uri("/products") .header("content-type", "application/json") .body(Body::from(product1_data.to_string())) .unwrap(), ) .await .unwrap(); app.clone() .oneshot( Request::builder() .method("POST") .uri("/products") .header("content-type", "application/json") .body(Body::from(product2_data.to_string())) .unwrap(), ) .await .unwrap(); // List products let response = app .oneshot( Request::builder() .method("GET") .uri("/products") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let products: Vec = extract_json(response).await; assert_eq!(products.len(), 2); assert!(products.iter().any(|p| p.name() == "Product 1")); assert!(products.iter().any(|p| p.name() == "Product 2")); } /// Tests updating a product with all fields. #[tokio::test] async fn test_update_product() { let app = create_test_app(); // First create a product let create_data = json!({ "name": "Old Product", "description": "Old Description" }); let create_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/products") .header("content-type", "application/json") .body(Body::from(create_data.to_string())) .unwrap(), ) .await .unwrap(); let created_product: Product = extract_json(create_response).await; // Update the product let update_data = json!({ "name": "New Product", "description": "New Description" }); let response = app .oneshot( Request::builder() .method("PUT") .uri(format!("/products/{}", created_product.id())) .header("content-type", "application/json") .body(Body::from(update_data.to_string())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let product: Product = extract_json(response).await; assert_eq!(product.id(), created_product.id()); assert_eq!(product.name(), "New Product"); assert_eq!(product.description(), "New Description"); } /// Tests partial product updates. #[tokio::test] async fn test_update_product_partial() { let app = create_test_app(); // First create a product let create_data = json!({ "name": "Test Product", "description": "Test Description" }); let create_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/products") .header("content-type", "application/json") .body(Body::from(create_data.to_string())) .unwrap(), ) .await .unwrap(); let created_product: Product = extract_json(create_response).await; // Update only name let update_data = json!({ "name": "New Product" }); let response = app .oneshot( Request::builder() .method("PUT") .uri(format!("/products/{}", created_product.id())) .header("content-type", "application/json") .body(Body::from(update_data.to_string())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); let product: Product = extract_json(response).await; assert_eq!(product.id(), created_product.id()); assert_eq!(product.name(), "New Product"); assert_eq!(product.description(), "Test Description"); // Should remain unchanged } /// Tests updating a non-existent product. #[tokio::test] async fn test_update_product_not_found() { let app = create_test_app(); let non_existent_id = Uuid::new_v4(); let update_data = json!({ "name": "New Product" }); let response = app .oneshot( Request::builder() .method("PUT") .uri(format!("/products/{}", non_existent_id)) .header("content-type", "application/json") .body(Body::from(update_data.to_string())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } /// Tests product deletion. #[tokio::test] async fn test_delete_product() { let app = create_test_app(); // First create a product let create_data = json!({ "name": "Test Product", "description": "Test Description" }); let create_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/products") .header("content-type", "application/json") .body(Body::from(create_data.to_string())) .unwrap(), ) .await .unwrap(); let created_product: Product = extract_json(create_response).await; // Delete the product let response = app .clone() .oneshot( Request::builder() .method("DELETE") .uri(format!("/products/{}", created_product.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::NO_CONTENT); // Verify product is deleted let get_response = app .oneshot( Request::builder() .method("GET") .uri(format!("/products/{}", created_product.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(get_response.status(), StatusCode::NOT_FOUND); } /// Tests deleting a non-existent product. #[tokio::test] async fn test_delete_product_not_found() { let app = create_test_app(); let non_existent_id = Uuid::new_v4(); let response = app .oneshot( Request::builder() .method("DELETE") .uri(format!("/products/{}", non_existent_id)) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::NOT_FOUND); } } mod integration_tests { //! # Integration Tests //! //! End-to-end tests that verify complete entity lifecycles //! including create, read, update, and delete operations. use super::*; /// Tests complete user lifecycle (create, read, update, delete). #[tokio::test] async fn test_user_lifecycle() { let app = create_test_app(); // Create user let create_data = json!({ "username": "testuser", "email": "test@example.com" }); let create_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/users") .header("content-type", "application/json") .body(Body::from(create_data.to_string())) .unwrap(), ) .await .unwrap(); assert_eq!(create_response.status(), StatusCode::CREATED); let user: User = extract_json(create_response).await; // Get user 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()); // Update user let update_data = json!({ "username": "updateduser" }); let update_response = app .clone() .oneshot( Request::builder() .method("PUT") .uri(format!("/users/{}", user.id())) .header("content-type", "application/json") .body(Body::from(update_data.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(), "updateduser"); // Delete user 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 deletion let verify_response = app .oneshot( Request::builder() .method("GET") .uri(format!("/users/{}", user.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(verify_response.status(), StatusCode::NOT_FOUND); } /// Tests complete product lifecycle (create, read, update, delete). #[tokio::test] async fn test_product_lifecycle() { let app = create_test_app(); // Create product let create_data = json!({ "name": "Test Product", "description": "Test Description" }); let create_response = app .clone() .oneshot( Request::builder() .method("POST") .uri("/products") .header("content-type", "application/json") .body(Body::from(create_data.to_string())) .unwrap(), ) .await .unwrap(); assert_eq!(create_response.status(), StatusCode::CREATED); let product: Product = extract_json(create_response).await; // Get product 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()); // Update product let update_data = json!({ "name": "Updated Product" }); let update_response = app .clone() .oneshot( Request::builder() .method("PUT") .uri(format!("/products/{}", product.id())) .header("content-type", "application/json") .body(Body::from(update_data.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(), "Updated Product"); // Delete product 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 deletion let verify_response = app .oneshot( Request::builder() .method("GET") .uri(format!("/products/{}", product.id())) .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(verify_response.status(), StatusCode::NOT_FOUND); } } }