diff --git a/backend/crates/postgres/Cargo.toml b/backend/crates/postgres/Cargo.toml index b73ca89..de4abc8 100644 --- a/backend/crates/postgres/Cargo.toml +++ b/backend/crates/postgres/Cargo.toml @@ -12,3 +12,6 @@ sqlx = { workspace = true } tokio = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } + +[dev-dependencies] +futures = "0.3" diff --git a/backend/crates/postgres/TEST_README.md b/backend/crates/postgres/TEST_README.md new file mode 100644 index 0000000..a5cb155 --- /dev/null +++ b/backend/crates/postgres/TEST_README.md @@ -0,0 +1,206 @@ +# Postgres Crate Tests + +This document explains how to set up and run the unit tests for the postgres crate. + +## Prerequisites + +1. **PostgreSQL Database**: You need a PostgreSQL database running locally or remotely +2. **Test Database**: Create a test database (e.g., `sharenet_test`) +3. **Environment Variables**: Set up the `DATABASE_URL` environment variable + +## Setup + +### 1. Database Setup + +Create a test database: + +```sql +CREATE DATABASE sharenet_test; +``` + +### 2. Environment Configuration + +Set the `DATABASE_URL` environment variable: + +```bash +export DATABASE_URL="postgres://username:password@localhost:5432/sharenet_test" +``` + +Or create a `.env` file in the postgres crate directory: + +```env +DATABASE_URL=postgres://username:password@localhost:5432/sharenet_test +``` + +### 3. SQLx Setup + +If you're using SQLx with offline mode, prepare the query cache: + +```bash +cd backend/crates/postgres +cargo sqlx prepare +``` + +## Migrations and Schema + +- The `users` table now enforces a unique constraint on the `username` field. This is implemented via a separate migration file: + - `20240101000001_add_username_unique_constraint.sql` +- If you have already run previous migrations, make sure to apply the new migration: + +```bash +cd backend/crates/postgres +sqlx migrate run +``` + +## Running Tests + +### Run All Tests + +```bash +cd backend/crates/postgres +cargo test +``` + +### Run Specific Test Modules + +```bash +# Run only user repository tests +cargo test user_repository_tests + +# Run only product repository tests +cargo test product_repository_tests + +# Run only service tests +cargo test service_tests + +# Run only error handling tests +cargo test error_handling_tests +``` + +### Run Tests with Output + +```bash +cargo test -- --nocapture +``` + +## Test Isolation + +- The test setup function (`setup_test_db`) now always cleans up the database at the start of each test, ensuring a clean state for every test run. +- For full isolation and to avoid concurrency issues, run tests with a single thread: + +```bash +cargo test -- --test-threads=1 +``` + +## Test Structure + +The tests are organized into the following modules: + +### 1. User Repository Tests (`user_repository_tests`) + +Tests for the `PostgresUserRepository` implementation: + +- **CRUD Operations**: Create, Read, Update, Delete users +- **Error Handling**: Not found scenarios, duplicate constraints +- **Edge Cases**: Empty results, partial updates + +### 2. Product Repository Tests (`product_repository_tests`) + +Tests for the `PostgresProductRepository` implementation: + +- **CRUD Operations**: Create, Read, Update, Delete products +- **Error Handling**: Not found scenarios +- **Edge Cases**: Empty results, partial updates + +### 3. Service Tests (`service_tests`) + +Tests for the service layer that wraps the repositories: + +- **Full Workflow**: Complete CRUD operations through services +- **Integration**: Repository and service interaction + +### 4. Error Handling Tests (`error_handling_tests`) + +Tests for error scenarios: + +- **Database Connection**: Invalid connection strings +- **Concurrent Access**: Multiple simultaneous operations + +## Test Database Management + +Each test automatically: + +1. **Sets up** a fresh database connection +2. **Runs migrations** to ensure schema is up to date +3. **Cleans up** test data after completion + +The test setup functions handle: +- Database connection pooling +- Migration execution +- Data cleanup + +## Troubleshooting + +### Common Issues + +1. **Database Connection Failed** + - Verify PostgreSQL is running + - Check `DATABASE_URL` format + - Ensure database exists + +2. **Migration Errors** + - Ensure migrations are up to date + - Check database permissions + +3. **SQLx Compilation Errors** + - Run `cargo sqlx prepare` to update query cache + - Or set `DATABASE_URL` for online mode + +### Debug Mode + +To run tests with more verbose output: + +```bash +RUST_LOG=debug cargo test -- --nocapture +``` + +## Test Data + +Tests use isolated data and clean up after themselves. Each test: + +- Creates its own test data +- Verifies operations work correctly +- Cleans up all created data + +This ensures tests are independent and don't interfere with each other. + +## Performance Considerations + +- Tests use a connection pool with max 5 connections +- Each test runs in isolation +- Database operations are async for better performance + +## Continuous Integration + +For CI/CD pipelines: + +1. Set up a PostgreSQL service container +2. Configure `DATABASE_URL` environment variable +3. Run `cargo test` in the postgres crate directory + +Example GitHub Actions setup: + +```yaml +- name: Setup PostgreSQL + uses: Harmon758/postgresql-action@v1.0.0 + with: + postgresql version: '15' + postgresql db: 'sharenet_test' + +- name: Run Postgres Tests + env: + DATABASE_URL: postgres://postgres:postgres@localhost:5432/sharenet_test + run: | + cd backend/crates/postgres + cargo test +``` \ No newline at end of file diff --git a/backend/crates/postgres/run_tests.sh b/backend/crates/postgres/run_tests.sh new file mode 100644 index 0000000..4bc1917 --- /dev/null +++ b/backend/crates/postgres/run_tests.sh @@ -0,0 +1,123 @@ +#!/bin/bash + +# Postgres Crate Test Runner +# This script helps set up and run the postgres crate tests + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +echo -e "${GREEN}Postgres Crate Test Runner${NC}" +echo "================================" + +# Check if DATABASE_URL is set +if [ -z "$DATABASE_URL" ]; then + echo -e "${YELLOW}DATABASE_URL not set, using default test database${NC}" + export DATABASE_URL="postgres://postgres:password@localhost:5432/sharenet_test" +fi + +echo -e "${GREEN}Using database: $DATABASE_URL${NC}" + +# Check if we're in the right directory +if [ ! -f "Cargo.toml" ] || [ ! -f "src/lib.rs" ]; then + echo -e "${RED}Error: Must run this script from the postgres crate directory${NC}" + echo "Current directory: $(pwd)" + echo "Expected files: Cargo.toml, src/lib.rs" + exit 1 +fi + +# Function to check if PostgreSQL is running +check_postgres() { + echo -e "${YELLOW}Checking PostgreSQL connection...${NC}" + + # Try to connect to the database + if command -v psql &> /dev/null; then + if psql "$DATABASE_URL" -c "SELECT 1;" &> /dev/null; then + echo -e "${GREEN}PostgreSQL connection successful${NC}" + return 0 + else + echo -e "${RED}PostgreSQL connection failed${NC}" + return 1 + fi + else + echo -e "${YELLOW}psql not found, skipping connection check${NC}" + return 0 + fi +} + +# Function to run migrations +run_migrations() { + echo -e "${YELLOW}Running database migrations...${NC}" + + if command -v sqlx &> /dev/null; then + sqlx migrate run --database-url "$DATABASE_URL" || { + echo -e "${RED}Migration failed${NC}" + return 1 + } + echo -e "${GREEN}Migrations completed successfully${NC}" + else + echo -e "${YELLOW}sqlx CLI not found, migrations will be run by tests${NC}" + fi +} + +# Function to run tests +run_tests() { + echo -e "${GREEN}Running tests...${NC}" + + # Check if specific test pattern was provided + if [ $# -eq 0 ]; then + echo "Running all tests..." + cargo test + else + echo "Running tests matching: $1" + cargo test "$1" + fi +} + +# Main execution +main() { + # Check PostgreSQL connection + if ! check_postgres; then + echo -e "${RED}Please ensure PostgreSQL is running and accessible${NC}" + echo "You can start PostgreSQL with:" + echo " sudo systemctl start postgresql" + echo " or" + echo " docker run --name postgres-test -e POSTGRES_PASSWORD=password -e POSTGRES_DB=sharenet_test -p 5432:5432 -d postgres:15" + exit 1 + fi + + # Run migrations + run_migrations + + # Run tests + run_tests "$@" + + echo -e "${GREEN}Tests completed!${NC}" +} + +# Handle command line arguments +case "${1:-}" in + --help|-h) + echo "Usage: $0 [test_pattern]" + echo "" + echo "Options:" + echo " test_pattern Run only tests matching this pattern" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " $0 # Run all tests" + echo " $0 user_repository # Run user repository tests" + echo " $0 create # Run tests with 'create' in the name" + echo "" + echo "Environment:" + echo " DATABASE_URL Database connection string (optional)" + exit 0 + ;; + *) + main "$@" + ;; +esac \ No newline at end of file diff --git a/backend/crates/postgres/src/lib.rs b/backend/crates/postgres/src/lib.rs index 278dee2..4e33475 100644 --- a/backend/crates/postgres/src/lib.rs +++ b/backend/crates/postgres/src/lib.rs @@ -233,3 +233,694 @@ impl Repository for PostgresProductRepository { pub type PostgresUserService = Service; pub type PostgresProductService = Service; + +#[cfg(test)] +mod tests { + use super::*; + use sqlx::PgPool; + use sqlx::postgres::PgPoolOptions; + use std::env; + use chrono::Utc; + + // Test database setup + 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"); + + // Run migrations + sqlx::migrate!("../../migrations") + .run(&pool) + .await + .expect("Failed to run migrations"); + + // Clean up any existing test data + cleanup_test_data(&pool).await; + + pool + } + + // Clean up test data + async fn cleanup_test_data(pool: &PgPool) { + sqlx::query("DELETE FROM products").execute(pool).await.ok(); + sqlx::query("DELETE FROM users").execute(pool).await.ok(); + } + + mod user_repository_tests { + use super::*; + + #[tokio::test] + async fn test_create_user() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + + let create_data = CreateUser { + username: "testuser".to_string(), + email: "test@example.com".to_string(), + }; + + let result = repo.create(create_data).await; + assert!(result.is_ok()); + + let user = result.unwrap(); + assert_eq!(user.username, "testuser"); + assert_eq!(user.email, "test@example.com"); + assert!(user.id != Uuid::nil()); + assert!(user.created_at <= Utc::now()); + assert!(user.updated_at <= Utc::now()); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_create_user_with_duplicate_username() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + + let create_data = CreateUser { + username: "duplicate_user".to_string(), + email: "test1@example.com".to_string(), + }; + + // Create first user + let result1 = repo.create(create_data.clone()).await; + assert!(result1.is_ok()); + + // Try to create second user with same username + let create_data2 = CreateUser { + username: "duplicate_user".to_string(), + email: "test2@example.com".to_string(), + }; + let result2 = repo.create(create_data2).await; + assert!(result2.is_err()); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_find_user_by_id() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + + let create_data = CreateUser { + username: "finduser".to_string(), + email: "find@example.com".to_string(), + }; + + let created_user = repo.create(create_data).await.unwrap(); + let found_user = repo.find_by_id(created_user.id).await; + + assert!(found_user.is_ok()); + let user = found_user.unwrap(); + assert_eq!(user.id, created_user.id); + assert_eq!(user.username, "finduser"); + assert_eq!(user.email, "find@example.com"); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_find_user_by_nonexistent_id() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + + let nonexistent_id = Uuid::new_v4(); + let result = repo.find_by_id(nonexistent_id).await; + + assert!(result.is_err()); + match result.unwrap_err() { + domain::DomainError::NotFound(msg) => { + assert!(msg.contains("User not found")); + } + _ => panic!("Expected NotFound error"), + } + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_find_all_users() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + + // Create multiple users + let _user1 = repo.create(CreateUser { + username: "user1".to_string(), + email: "user1@example.com".to_string(), + }).await.unwrap(); + + let _user2 = repo.create(CreateUser { + username: "user2".to_string(), + email: "user2@example.com".to_string(), + }).await.unwrap(); + + let users = repo.find_all().await.unwrap(); + assert_eq!(users.len(), 2); + + let usernames: Vec = users.iter().map(|u| u.username.clone()).collect(); + assert!(usernames.contains(&"user1".to_string())); + assert!(usernames.contains(&"user2".to_string())); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_find_all_users_empty() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + + let users = repo.find_all().await.unwrap(); + assert_eq!(users.len(), 0); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_update_user() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + + let create_data = CreateUser { + username: "updateuser".to_string(), + email: "update@example.com".to_string(), + }; + + let user = repo.create(create_data).await.unwrap(); + let original_updated_at = user.updated_at; + + // Update username only + let update_data = UpdateUser { + username: Some("updateduser".to_string()), + email: None, + }; + + let updated_user = repo.update(user.id, update_data).await.unwrap(); + assert_eq!(updated_user.username, "updateduser"); + assert_eq!(updated_user.email, "update@example.com"); // Should remain unchanged + assert!(updated_user.updated_at > original_updated_at); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_update_user_email_only() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + + let create_data = CreateUser { + username: "emailuser".to_string(), + email: "old@example.com".to_string(), + }; + + let user = repo.create(create_data).await.unwrap(); + + let update_data = UpdateUser { + username: None, + email: Some("new@example.com".to_string()), + }; + + let updated_user = repo.update(user.id, update_data).await.unwrap(); + assert_eq!(updated_user.username, "emailuser"); // Should remain unchanged + assert_eq!(updated_user.email, "new@example.com"); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_update_user_both_fields() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + + let create_data = CreateUser { + username: "bothuser".to_string(), + email: "both@example.com".to_string(), + }; + + let user = repo.create(create_data).await.unwrap(); + + let update_data = UpdateUser { + username: Some("newbothuser".to_string()), + email: Some("newboth@example.com".to_string()), + }; + + let updated_user = repo.update(user.id, update_data).await.unwrap(); + assert_eq!(updated_user.username, "newbothuser"); + assert_eq!(updated_user.email, "newboth@example.com"); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_update_nonexistent_user() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + + let nonexistent_id = Uuid::new_v4(); + let update_data = UpdateUser { + username: Some("nonexistent".to_string()), + email: None, + }; + + let result = repo.update(nonexistent_id, update_data).await; + assert!(result.is_err()); + match result.unwrap_err() { + domain::DomainError::NotFound(msg) => { + assert!(msg.contains("User not found")); + } + _ => panic!("Expected NotFound error"), + } + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_delete_user() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + + let create_data = CreateUser { + username: "deleteuser".to_string(), + email: "delete@example.com".to_string(), + }; + + let user = repo.create(create_data).await.unwrap(); + let user_id = user.id; + + // Verify user exists + let found_user = repo.find_by_id(user_id).await; + assert!(found_user.is_ok()); + + // Delete user + let delete_result = repo.delete(user_id).await; + assert!(delete_result.is_ok()); + + // Verify user no longer exists + let found_user_after_delete = repo.find_by_id(user_id).await; + assert!(found_user_after_delete.is_err()); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_delete_nonexistent_user() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + + let nonexistent_id = Uuid::new_v4(); + let result = repo.delete(nonexistent_id).await; + + assert!(result.is_err()); + match result.unwrap_err() { + domain::DomainError::NotFound(msg) => { + assert!(msg.contains("User not found")); + } + _ => panic!("Expected NotFound error"), + } + + cleanup_test_data(&pool).await; + } + } + + mod product_repository_tests { + use super::*; + + #[tokio::test] + async fn test_create_product() { + let pool = setup_test_db().await; + let repo = PostgresProductRepository::new(pool.clone()); + + let create_data = CreateProduct { + name: "Test Product".to_string(), + description: "Test Description".to_string(), + }; + + let result = repo.create(create_data).await; + assert!(result.is_ok()); + + let product = result.unwrap(); + assert_eq!(product.name, "Test Product"); + assert_eq!(product.description, "Test Description"); + assert!(product.id != Uuid::nil()); + assert!(product.created_at <= Utc::now()); + assert!(product.updated_at <= Utc::now()); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_find_product_by_id() { + let pool = setup_test_db().await; + let repo = PostgresProductRepository::new(pool.clone()); + + let create_data = CreateProduct { + name: "Find Product".to_string(), + description: "Find Description".to_string(), + }; + + let created_product = repo.create(create_data).await.unwrap(); + let found_product = repo.find_by_id(created_product.id).await; + + assert!(found_product.is_ok()); + let product = found_product.unwrap(); + assert_eq!(product.id, created_product.id); + assert_eq!(product.name, "Find Product"); + assert_eq!(product.description, "Find Description"); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_find_product_by_nonexistent_id() { + let pool = setup_test_db().await; + let repo = PostgresProductRepository::new(pool.clone()); + + let nonexistent_id = Uuid::new_v4(); + let result = repo.find_by_id(nonexistent_id).await; + + assert!(result.is_err()); + match result.unwrap_err() { + domain::DomainError::NotFound(msg) => { + assert!(msg.contains("Product not found")); + } + _ => panic!("Expected NotFound error"), + } + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_find_all_products() { + let pool = setup_test_db().await; + let repo = PostgresProductRepository::new(pool.clone()); + + // Create multiple products + let _product1 = repo.create(CreateProduct { + name: "Product 1".to_string(), + description: "Description 1".to_string(), + }).await.unwrap(); + + let _product2 = repo.create(CreateProduct { + name: "Product 2".to_string(), + description: "Description 2".to_string(), + }).await.unwrap(); + + let products = repo.find_all().await.unwrap(); + assert_eq!(products.len(), 2); + + let names: Vec = products.iter().map(|p| p.name.clone()).collect(); + assert!(names.contains(&"Product 1".to_string())); + assert!(names.contains(&"Product 2".to_string())); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_find_all_products_empty() { + let pool = setup_test_db().await; + let repo = PostgresProductRepository::new(pool.clone()); + + let products = repo.find_all().await.unwrap(); + assert_eq!(products.len(), 0); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_update_product() { + let pool = setup_test_db().await; + let repo = PostgresProductRepository::new(pool.clone()); + + let create_data = CreateProduct { + name: "Update Product".to_string(), + description: "Update Description".to_string(), + }; + + let product = repo.create(create_data).await.unwrap(); + let original_updated_at = product.updated_at; + + // Update name only + let update_data = UpdateProduct { + name: Some("Updated Product".to_string()), + description: None, + }; + + let updated_product = repo.update(product.id, update_data).await.unwrap(); + assert_eq!(updated_product.name, "Updated Product"); + assert_eq!(updated_product.description, "Update Description"); // Should remain unchanged + assert!(updated_product.updated_at > original_updated_at); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_update_product_description_only() { + let pool = setup_test_db().await; + let repo = PostgresProductRepository::new(pool.clone()); + + let create_data = CreateProduct { + name: "Desc Product".to_string(), + description: "Old Description".to_string(), + }; + + let product = repo.create(create_data).await.unwrap(); + + let update_data = UpdateProduct { + name: None, + description: Some("New Description".to_string()), + }; + + let updated_product = repo.update(product.id, update_data).await.unwrap(); + assert_eq!(updated_product.name, "Desc Product"); // Should remain unchanged + assert_eq!(updated_product.description, "New Description"); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_update_product_both_fields() { + let pool = setup_test_db().await; + let repo = PostgresProductRepository::new(pool.clone()); + + let create_data = CreateProduct { + name: "Both Product".to_string(), + description: "Both Description".to_string(), + }; + + let product = repo.create(create_data).await.unwrap(); + + let update_data = UpdateProduct { + name: Some("New Both Product".to_string()), + description: Some("New Both Description".to_string()), + }; + + let updated_product = repo.update(product.id, update_data).await.unwrap(); + assert_eq!(updated_product.name, "New Both Product"); + assert_eq!(updated_product.description, "New Both Description"); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_update_nonexistent_product() { + let pool = setup_test_db().await; + let repo = PostgresProductRepository::new(pool.clone()); + + let nonexistent_id = Uuid::new_v4(); + let update_data = UpdateProduct { + name: Some("Nonexistent Product".to_string()), + description: None, + }; + + let result = repo.update(nonexistent_id, update_data).await; + assert!(result.is_err()); + match result.unwrap_err() { + domain::DomainError::NotFound(msg) => { + assert!(msg.contains("Product not found")); + } + _ => panic!("Expected NotFound error"), + } + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_delete_product() { + let pool = setup_test_db().await; + let repo = PostgresProductRepository::new(pool.clone()); + + let create_data = CreateProduct { + name: "Delete Product".to_string(), + description: "Delete Description".to_string(), + }; + + let product = repo.create(create_data).await.unwrap(); + let product_id = product.id; + + // Verify product exists + let found_product = repo.find_by_id(product_id).await; + assert!(found_product.is_ok()); + + // Delete product + let delete_result = repo.delete(product_id).await; + assert!(delete_result.is_ok()); + + // Verify product no longer exists + let found_product_after_delete = repo.find_by_id(product_id).await; + assert!(found_product_after_delete.is_err()); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_delete_nonexistent_product() { + let pool = setup_test_db().await; + let repo = PostgresProductRepository::new(pool.clone()); + + let nonexistent_id = Uuid::new_v4(); + let result = repo.delete(nonexistent_id).await; + + assert!(result.is_err()); + match result.unwrap_err() { + domain::DomainError::NotFound(msg) => { + assert!(msg.contains("Product not found")); + } + _ => panic!("Expected NotFound error"), + } + + cleanup_test_data(&pool).await; + } + } + + mod service_tests { + use super::*; + use application::UseCase; + + #[tokio::test] + async fn test_postgres_user_service() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + let service = PostgresUserService::new(repo); + + let create_data = CreateUser { + username: "serviceuser".to_string(), + email: "service@example.com".to_string(), + }; + + // Test create + let user = service.create(create_data).await.unwrap(); + assert_eq!(user.username, "serviceuser"); + + // Test get + let found_user = service.get(user.id).await.unwrap(); + assert_eq!(found_user.id, user.id); + + // Test list + let users = service.list().await.unwrap(); + assert_eq!(users.len(), 1); + + // Test update + let update_data = UpdateUser { + username: Some("updatedserviceuser".to_string()), + email: None, + }; + let updated_user = service.update(user.id, update_data).await.unwrap(); + assert_eq!(updated_user.username, "updatedserviceuser"); + + // Test delete + let delete_result = service.delete(user.id).await; + assert!(delete_result.is_ok()); + + cleanup_test_data(&pool).await; + } + + #[tokio::test] + async fn test_postgres_product_service() { + let pool = setup_test_db().await; + let repo = PostgresProductRepository::new(pool.clone()); + let service = PostgresProductService::new(repo); + + let create_data = CreateProduct { + name: "Service Product".to_string(), + description: "Service Description".to_string(), + }; + + // Test create + let product = service.create(create_data).await.unwrap(); + assert_eq!(product.name, "Service Product"); + + // Test get + let found_product = service.get(product.id).await.unwrap(); + assert_eq!(found_product.id, product.id); + + // Test list + let products = service.list().await.unwrap(); + assert_eq!(products.len(), 1); + + // Test update + let update_data = UpdateProduct { + name: Some("Updated Service Product".to_string()), + description: None, + }; + let updated_product = service.update(product.id, update_data).await.unwrap(); + assert_eq!(updated_product.name, "Updated Service Product"); + + // Test delete + let delete_result = service.delete(product.id).await; + assert!(delete_result.is_ok()); + + cleanup_test_data(&pool).await; + } + } + + mod error_handling_tests { + use super::*; + + #[tokio::test] + async fn test_database_connection_error() { + // Test with invalid database URL + let invalid_pool = PgPoolOptions::new() + .max_connections(1) + .connect("postgres://invalid:invalid@localhost:5432/nonexistent") + .await; + + assert!(invalid_pool.is_err()); + } + + #[tokio::test] + async fn test_concurrent_access() { + let pool = setup_test_db().await; + let repo = PostgresUserRepository::new(pool.clone()); + + let create_data = CreateUser { + username: "concurrentuser".to_string(), + email: "concurrent@example.com".to_string(), + }; + + // Create a user + let user = repo.create(create_data).await.unwrap(); + + // Simulate concurrent reads + let handles: Vec<_> = (0..10) + .map(|_| { + let repo_clone = repo.clone(); + let user_id = user.id; + tokio::spawn(async move { + repo_clone.find_by_id(user_id).await + }) + }) + .collect(); + + let results = futures::future::join_all(handles).await; + for result in results { + assert!(result.unwrap().is_ok()); + } + + cleanup_test_data(&pool).await; + } + } +} diff --git a/backend/crates/postgres/tests/integration_test_setup.rs b/backend/crates/postgres/tests/integration_test_setup.rs new file mode 100644 index 0000000..a5285e8 --- /dev/null +++ b/backend/crates/postgres/tests/integration_test_setup.rs @@ -0,0 +1,43 @@ +/* + * 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 sqlx::PgPool; +use sqlx::postgres::PgPoolOptions; +use std::env; + +pub async fn setup_test_database() -> PgPool { + let database_url = env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5432/sharenet_test".to_string()); + + let pool = PgPoolOptions::new() + .max_connections(5) + .connect(&database_url) + .await + .expect("Failed to connect to test database"); + + // Run migrations + sqlx::migrate!("../../migrations") + .run(&pool) + .await + .expect("Failed to run migrations"); + + pool +} + +pub async fn cleanup_test_data(pool: &PgPool) { + sqlx::query("DELETE FROM products").execute(pool).await.ok(); + sqlx::query("DELETE FROM users").execute(pool).await.ok(); +} + +pub fn get_test_database_url() -> String { + env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://postgres:postgres@localhost:5432/sharenet_test".to_string()) +} \ No newline at end of file diff --git a/backend/migrations/20240101000001_add_username_unique_constraint.sql b/backend/migrations/20240101000001_add_username_unique_constraint.sql new file mode 100644 index 0000000..3b13558 --- /dev/null +++ b/backend/migrations/20240101000001_add_username_unique_constraint.sql @@ -0,0 +1,2 @@ +-- Add unique constraint to username field +ALTER TABLE users ADD CONSTRAINT users_username_unique UNIQUE (username); \ No newline at end of file