Add cross-crate integration tests to a new integration-tests crate
This commit is contained in:
parent
05683d5f32
commit
243f9f9aef
3 changed files with 699 additions and 0 deletions
32
backend/crates/integration-tests/Cargo.toml
Normal file
32
backend/crates/integration-tests/Cargo.toml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
[package]
|
||||||
|
name = "integration-tests"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# Workspace dependencies
|
||||||
|
tokio.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
uuid.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
sqlx.workspace = true
|
||||||
|
axum.workspace = true
|
||||||
|
tower.workspace = true
|
||||||
|
serial_test = "2.0"
|
||||||
|
|
||||||
|
# Local crates
|
||||||
|
api = { path = "../api" }
|
||||||
|
application = { path = "../application" }
|
||||||
|
domain = { path = "../domain" }
|
||||||
|
postgres = { path = "../postgres" }
|
||||||
|
|
||||||
|
# Additional test dependencies
|
||||||
|
hyper = { version = "1.0", features = ["full"] }
|
||||||
|
tower-http = { version = "0.5", features = ["cors", "trace"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
serial_test = "2.0"
|
654
backend/crates/integration-tests/src/api_postgres_tests.rs
Normal file
654
backend/crates/integration-tests/src/api_postgres_tests.rs
Normal file
|
@ -0,0 +1,654 @@
|
||||||
|
/*
|
||||||
|
* 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 sqlx::PgPool;
|
||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
use std::env;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
async fn setup_test_db() -> PgPool {
|
||||||
|
let database_url = env::var("DATABASE_URL")
|
||||||
|
.unwrap_or_else(|_| "postgres://postgres:password@localhost:5432/sharenet_test".to_string());
|
||||||
|
let pool = PgPoolOptions::new()
|
||||||
|
.max_connections(5)
|
||||||
|
.connect(&database_url)
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect to test database");
|
||||||
|
sqlx::migrate!("../../migrations")
|
||||||
|
.run(&pool)
|
||||||
|
.await
|
||||||
|
.expect("Failed to run migrations");
|
||||||
|
cleanup_test_data(&pool).await;
|
||||||
|
pool
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cleanup_test_data(pool: &PgPool) {
|
||||||
|
let mut tx = pool.begin().await.expect("Failed to begin transaction");
|
||||||
|
sqlx::query("DELETE FROM products").execute(&mut *tx).await.expect("Failed to delete products");
|
||||||
|
sqlx::query("DELETE FROM users").execute(&mut *tx).await.expect("Failed to delete users");
|
||||||
|
tx.commit().await.expect("Failed to commit cleanup transaction");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unique_test_data(prefix: &str) -> (String, String) {
|
||||||
|
let id = Uuid::new_v4().to_string()[..8].to_string();
|
||||||
|
(format!("{}_{}", prefix, id), format!("{}_test@example.com", prefix))
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.clone()).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.clone()).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);
|
||||||
|
}
|
13
backend/crates/integration-tests/src/lib.rs
Normal file
13
backend/crates/integration-tests/src/lib.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* 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>
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod api_postgres_tests;
|
Loading…
Add table
Reference in a new issue