Add postgres crate unit tests

This commit is contained in:
continuist 2025-06-22 19:25:16 -04:00
parent 3e7f6fea8e
commit 816f2d46b8
6 changed files with 1068 additions and 0 deletions

View file

@ -12,3 +12,6 @@ sqlx = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
[dev-dependencies]
futures = "0.3"

View file

@ -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
```

View file

@ -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

View file

@ -233,3 +233,694 @@ impl Repository<Product> for PostgresProductRepository {
pub type PostgresUserService = Service<User, PostgresUserRepository>; pub type PostgresUserService = Service<User, PostgresUserRepository>;
pub type PostgresProductService = Service<Product, PostgresProductRepository>; pub type PostgresProductService = Service<Product, PostgresProductRepository>;
#[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<String> = 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<String> = 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;
}
}
}

View file

@ -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 <continuist02@gmail.com>
*/
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())
}

View file

@ -0,0 +1,2 @@
-- Add unique constraint to username field
ALTER TABLE users ADD CONSTRAINT users_username_unique UNIQUE (username);