sharenet/backend/crates/integration-tests/src/api_postgres_tests.rs
continuist 61117b6fa6
Some checks are pending
CI/CD Pipeline / Test Backend (push) Waiting to run
CI/CD Pipeline / Test Frontend (push) Waiting to run
CI/CD Pipeline / Build and Push Docker Images (push) Blocked by required conditions
CI/CD Pipeline / Deploy to Production (push) Blocked by required conditions
Created test_setup.rs file that consolidates test setup code for interface tests, including db migrations
2025-06-28 01:57:12 -04:00

628 lines
No EOL
20 KiB
Rust

/*
* 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 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<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(),
}
}
}
// 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<T: serde::de::DeserializeOwned>(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<AppState<Service<User, PostgresUserRepository>, Service<Product, PostgresProductRepository>>>,
Json(data): Json<CreateUser>,
) -> 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<AppState<Service<User, PostgresUserRepository>, Service<Product, PostgresProductRepository>>>,
axum::extract::Path(id): axum::extract::Path<Uuid>,
) -> 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<AppState<Service<User, PostgresUserRepository>, Service<Product, PostgresProductRepository>>>,
) -> 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<AppState<Service<User, PostgresUserRepository>, Service<Product, PostgresProductRepository>>>,
axum::extract::Path(id): axum::extract::Path<Uuid>,
Json(data): Json<UpdateUser>,
) -> 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<AppState<Service<User, PostgresUserRepository>, Service<Product, PostgresProductRepository>>>,
axum::extract::Path(id): axum::extract::Path<Uuid>,
) -> 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<AppState<Service<User, PostgresUserRepository>, Service<Product, PostgresProductRepository>>>,
Json(data): Json<CreateProduct>,
) -> 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<AppState<Service<User, PostgresUserRepository>, Service<Product, PostgresProductRepository>>>,
axum::extract::Path(id): axum::extract::Path<Uuid>,
) -> 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<AppState<Service<User, PostgresUserRepository>, Service<Product, PostgresProductRepository>>>,
) -> 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<AppState<Service<User, PostgresUserRepository>, Service<Product, PostgresProductRepository>>>,
axum::extract::Path(id): axum::extract::Path<Uuid>,
Json(data): Json<UpdateProduct>,
) -> 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<AppState<Service<User, PostgresUserRepository>, Service<Product, PostgresProductRepository>>>,
axum::extract::Path(id): axum::extract::Path<Uuid>,
) -> 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<User> = extract_json(list_response).await;
assert_eq!(users.len(), 2);
let usernames: Vec<String> = 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<Product> = extract_json(list_response).await;
assert_eq!(products.len(), 2);
let names: Vec<String> = 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);
}