1516 lines
50 KiB
Rust
1516 lines
50 KiB
Rust
//! # 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
|
|
//!
|
|
//! ### 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<User> and UseCase<Product>
|
|
//! #[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 <continuist02@gmail.com>
|
|
*/
|
|
|
|
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<U, P> {
|
|
user_service: Arc<U>,
|
|
product_service: Arc<P>,
|
|
}
|
|
|
|
impl<U, P> Clone for AppState<U, P>
|
|
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<U, P>(addr: SocketAddr, user_service: U, product_service: P)
|
|
where
|
|
U: UseCase<User> + Clone + Send + Sync + 'static,
|
|
P: UseCase<Product> + 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("/users", post(create_user::<U>))
|
|
.route("/users/:id", get(get_user::<U>))
|
|
.route("/users", get(list_users::<U>))
|
|
.route("/users/:id", put(update_user::<U>))
|
|
.route("/users/:id", delete(delete_user::<U>))
|
|
.route("/products", post(create_product::<P>))
|
|
.route("/products/:id", get(get_product::<P>))
|
|
.route("/products", get(list_products::<P>))
|
|
.route("/products/:id", put(update_product::<P>))
|
|
.route("/products/:id", delete(delete_product::<P>))
|
|
.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<U>(
|
|
State(state): State<AppState<U, impl UseCase<Product>>>,
|
|
Json(data): Json<CreateUser>,
|
|
) -> impl IntoResponse
|
|
where
|
|
U: UseCase<User>,
|
|
{
|
|
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<U>(
|
|
State(state): State<AppState<U, impl UseCase<Product>>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> impl IntoResponse
|
|
where
|
|
U: UseCase<User>,
|
|
{
|
|
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<U>(
|
|
State(state): State<AppState<U, impl UseCase<Product>>>,
|
|
) -> impl IntoResponse
|
|
where
|
|
U: UseCase<User>,
|
|
{
|
|
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<U>(
|
|
State(state): State<AppState<U, impl UseCase<Product>>>,
|
|
Path(id): Path<Uuid>,
|
|
Json(data): Json<UpdateUser>,
|
|
) -> impl IntoResponse
|
|
where
|
|
U: UseCase<User>,
|
|
{
|
|
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.
|
|
///
|
|
/// # Response
|
|
/// - `204 No Content` - User successfully deleted
|
|
/// - `404 Not Found` - User with specified ID not found
|
|
async fn delete_user<U>(
|
|
State(state): State<AppState<U, impl UseCase<Product>>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> impl IntoResponse
|
|
where
|
|
U: UseCase<User>,
|
|
{
|
|
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<P>(
|
|
State(state): State<AppState<impl UseCase<User>, P>>,
|
|
Json(data): Json<CreateProduct>,
|
|
) -> impl IntoResponse
|
|
where
|
|
P: UseCase<Product>,
|
|
{
|
|
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<P>(
|
|
State(state): State<AppState<impl UseCase<User>, P>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> impl IntoResponse
|
|
where
|
|
P: UseCase<Product>,
|
|
{
|
|
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<P>(
|
|
State(state): State<AppState<impl UseCase<User>, P>>,
|
|
) -> impl IntoResponse
|
|
where
|
|
P: UseCase<Product>,
|
|
{
|
|
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<P>(
|
|
State(state): State<AppState<impl UseCase<User>, P>>,
|
|
Path(id): Path<Uuid>,
|
|
Json(data): Json<UpdateProduct>,
|
|
) -> impl IntoResponse
|
|
where
|
|
P: UseCase<Product>,
|
|
{
|
|
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.
|
|
///
|
|
/// # Response
|
|
/// - `204 No Content` - Product successfully deleted
|
|
/// - `404 Not Found` - Product with specified ID not found
|
|
async fn delete_product<P>(
|
|
State(state): State<AppState<impl UseCase<User>, P>>,
|
|
Path(id): Path<Uuid>,
|
|
) -> impl IntoResponse
|
|
where
|
|
P: UseCase<Product>,
|
|
{
|
|
match state.product_service.delete(id).await {
|
|
Ok(_) => StatusCode::NO_CONTENT.into_response(),
|
|
Err(e) => (StatusCode::NOT_FOUND, e.to_string()).into_response(),
|
|
}
|
|
}
|
|
|
|
#[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<User>` trait using an in-memory HashMap
|
|
/// for storing test data. Provides thread-safe access via `RwLock`.
|
|
#[derive(Clone)]
|
|
struct MockUserService {
|
|
users: Arc<RwLock<HashMap<Uuid, User>>>,
|
|
}
|
|
|
|
impl MockUserService {
|
|
/// Creates a new mock user service with empty storage.
|
|
fn new() -> Self {
|
|
Self {
|
|
users: Arc::new(RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl UseCase<User> for MockUserService {
|
|
fn create(&self, data: CreateUser) -> impl std::future::Future<Output = Result<User, ApplicationError>> + 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<Output = Result<User, ApplicationError>> + 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<Output = Result<Vec<User>, 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<Output = Result<User, ApplicationError>> + 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<Output = Result<(), ApplicationError>> + 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<Product>` trait using an in-memory HashMap
|
|
/// for storing test data. Provides thread-safe access via `RwLock`.
|
|
#[derive(Clone)]
|
|
struct MockProductService {
|
|
products: Arc<RwLock<HashMap<Uuid, Product>>>,
|
|
}
|
|
|
|
impl MockProductService {
|
|
/// Creates a new mock product service with empty storage.
|
|
fn new() -> Self {
|
|
Self {
|
|
products: Arc::new(RwLock::new(HashMap::new())),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl UseCase<Product> for MockProductService {
|
|
fn create(&self, data: CreateProduct) -> impl std::future::Future<Output = Result<Product, ApplicationError>> + 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<Output = Result<Product, ApplicationError>> + 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<Output = Result<Vec<Product>, 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<Output = Result<Product, ApplicationError>> + 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<Output = Result<(), ApplicationError>> + 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("/users", post(create_user::<MockUserService>))
|
|
.route("/users/:id", get(get_user::<MockUserService>))
|
|
.route("/users", get(list_users::<MockUserService>))
|
|
.route("/users/:id", put(update_user::<MockUserService>))
|
|
.route("/users/:id", delete(delete_user::<MockUserService>))
|
|
.route("/products", post(create_product::<MockProductService>))
|
|
.route("/products/:id", get(get_product::<MockProductService>))
|
|
.route("/products", get(list_products::<MockProductService>))
|
|
.route("/products/:id", put(update_product::<MockProductService>))
|
|
.route("/products/:id", delete(delete_product::<MockProductService>))
|
|
.with_state(state)
|
|
}
|
|
|
|
/// Extracts JSON data from an HTTP response.
|
|
///
|
|
/// Helper function for tests to deserialize response bodies.
|
|
async fn extract_json<T: serde::de::DeserializeOwned>(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<User> = 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 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<Product> = 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);
|
|
}
|
|
}
|
|
}
|