Add postgres crate unit tests
This commit is contained in:
parent
3e7f6fea8e
commit
816f2d46b8
6 changed files with 1068 additions and 0 deletions
|
@ -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"
|
||||||
|
|
206
backend/crates/postgres/TEST_README.md
Normal file
206
backend/crates/postgres/TEST_README.md
Normal 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
|
||||||
|
```
|
123
backend/crates/postgres/run_tests.sh
Normal file
123
backend/crates/postgres/run_tests.sh
Normal 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
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
43
backend/crates/postgres/tests/integration_test_setup.rs
Normal file
43
backend/crates/postgres/tests/integration_test_setup.rs
Normal 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())
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- Add unique constraint to username field
|
||||||
|
ALTER TABLE users ADD CONSTRAINT users_username_unique UNIQUE (username);
|
Loading…
Add table
Reference in a new issue