diff --git a/api/documentation/testing.mdc b/api/documentation/testing.mdc index 0df260c16..88ad27f0d 100644 --- a/api/documentation/testing.mdc +++ b/api/documentation/testing.mdc @@ -22,31 +22,25 @@ Always prefer dependency injection patterns to enable easy mocking of dependenci ### Test Utilities and Setup -The project provides a comprehensive test infrastructure that offers standard utilities for database tests, permission testing, and asset management. These utilities are located in the `libs/database/tests/common` directory. +#### Core Test Types -#### Core Test Infrastructure Components - -1. **TestDb**: Central database test utility for connections and cleanup -2. **PermissionTestHelpers**: Utilities for testing permissions -3. **AssetTestHelpers**: Utilities for creating and managing test assets -4. **AuthenticatedUser**: Test user representation for auth scenarios - -#### TestDb - -The `TestDb` struct is the foundation of the test infrastructure, providing database connections, test isolation via unique IDs, and cleanup functionality: +The test infrastructure provides several core test utilities for consistent test setup: ```rust +// Example of TestDb and TestSetup implementation pub struct TestDb { - pub test_id: String, // Unique identifier for test isolation - pub organization_id: Uuid, // Test organization ID - pub user_id: Uuid, // Test user ID - initialized: bool, // Tracks initialization state + pub test_id: String, + pub organization_id: Uuid, + pub user_id: Uuid, + initialized: bool, } impl TestDb { - /// Creates a new test database environment pub async fn new() -> Result { - // Uses the pools already initialized in the test framework + // Load environment variables + dotenv().ok(); + + // Initialize test pools using existing pools let test_id = format!("test-{}", Uuid::new_v4()); let organization_id = Uuid::new_v4(); let user_id = Uuid::new_v4(); @@ -59,199 +53,133 @@ impl TestDb { }) } - /// Gets a database connection for testing - pub async fn diesel_conn(&self) -> Result> { + // Get connections from pre-initialized pools + pub async fn diesel_conn(&self) -> Result { get_pg_pool() .get() .await .map_err(|e| anyhow!("Failed to get diesel connection: {}", e)) } - /// Creates a mock authenticated user for testing - pub fn user(&self) -> AuthenticatedUser { - AuthenticatedUser { - id: self.user_id, - organization_id: self.organization_id, - // ... other fields ... - } + pub async fn sqlx_conn(&self) -> Result { + get_sqlx_pool() + .acquire() + .await + .map_err(|e| anyhow!("Failed to get sqlx connection: {}", e)) } - /// Creates test data including users and organizations + pub async fn redis_conn(&self) -> Result { + get_redis_pool() + .get() + .await + .map_err(|e| antml!("Failed to get redis connection: {}", e)) + } + + // Create test organization pub async fn create_organization(&self) -> Result { - // Implementation for creating a test organization + let org = Organization { + id: Uuid::new_v4(), + name: "Test Organization".to_string(), + domain: Some("test.org".to_string()), + created_at: Utc::now(), + updated_at: Utc::now(), + deleted_at: None, + }; + + let mut conn = self.diesel_conn().await?; + diesel::insert_into(organizations::table) + .values(&org) + .execute(&mut conn) + .await?; + + Ok(org) } - + + // Create test user pub async fn create_user(&self) -> Result { - // Implementation for creating a test user + let user = User { + id: Uuid::new_v4(), + email: format!("test-{}@example.com", Uuid::new_v4()), + name: Some("Test User".to_string()), + config: json!({}), + created_at: Utc::now(), + updated_at: Utc::now(), + attributes: json!({}), + avatar_url: None, + }; + + let mut conn = self.diesel_conn().await?; + diesel::insert_into(users::table) + .values(&user) + .execute(&mut conn) + .await?; + + Ok(user) } - - /// Creates a user-organization relationship + + // Create user-organization relationship pub async fn create_user_to_org( &self, user_id: Uuid, - org_id: Uuid, + org_id: Uuid, role: UserOrganizationRole, ) -> Result { - // Implementation for creating user-org relationship + let user_org = UserToOrganization { + user_id, + organization_id: org_id, + role, + sharing_setting: SharingSetting::Private, + edit_sql: true, + upload_csv: true, + export_assets: true, + email_slack_enabled: true, + created_at: Utc::now(), + updated_at: Utc::now(), + deleted_at: None, + created_by: user_id, + updated_by: user_id, + deleted_by: None, + status: UserOrganizationStatus::Active, + }; + + let mut conn = self.diesel_conn().await?; + diesel::insert_into(users_to_organizations::table) + .values(&user_org) + .execute(&mut conn) + .await?; + + Ok(user_org) } - - /// Creates an authenticated user with organization + + // Create authenticated user for testing pub async fn create_authenticated_user( &self, role: Option, ) -> Result<(AuthenticatedUser, Organization)> { - // Implementation for creating authenticated test user - } - - /// Cleans up all test data - pub async fn cleanup(&self) -> Result<()> { - // Removes all data created with this test instance + let org = self.create_organization().await?; + let user = self.create_user().await?; + + let role = role.unwrap_or(UserOrganizationRole::Admin); + self.create_user_to_org(user.id, org.id, role).await?; + + let auth_user = AuthenticatedUser { + id: user.id, + email: user.email, + name: user.name, + organization_id: org.id, + role, + sharing_setting: SharingSetting::Private, + edit_sql: true, + upload_csv: true, + export_assets: true, + email_slack_enabled: true, + }; + + Ok((auth_user, org)) } } -``` -#### AuthenticatedUser - -The `AuthenticatedUser` struct provides a simplified version of the application's authenticated user for testing: - -```rust -#[derive(Debug, Clone)] -pub struct AuthenticatedUser { - pub id: Uuid, - pub organization_id: Uuid, - pub email: String, - pub name: Option, - pub role: UserOrganizationRole, - // ... other fields ... -} - -impl AuthenticatedUser { - /// Creates a new random authenticated user with given role - pub fn mock_with_role(role: UserOrganizationRole) -> Self { - // Implementation - } - - /// Creates a new random administrator user - pub fn mock_admin() -> Self { - // Implementation - } - - // Other helper methods for different roles -} -``` - -#### PermissionTestHelpers - -The `PermissionTestHelpers` struct provides utilities for testing permissions: - -```rust -pub struct PermissionTestHelpers; - -impl PermissionTestHelpers { - /// Creates a permission for an asset - pub async fn create_permission( - test_db: &TestDb, - asset_id: Uuid, - asset_type: AssetType, - identity_id: Uuid, - identity_type: IdentityType, - role: AssetPermissionRole, - ) -> Result { - // Implementation - } - - /// Creates a user permission for an asset - pub async fn create_user_permission( - test_db: &TestDb, - asset_id: Uuid, - asset_type: AssetType, - user_id: Uuid, - role: AssetPermissionRole, - ) -> Result { - // Implementation - } - - /// Verifies that a permission exists with expected role - pub async fn verify_permission( - test_db: &TestDb, - asset_id: Uuid, - identity_id: Uuid, - expected_role: AssetPermissionRole, - ) -> Result<()> { - // Implementation - } - - /// Gets all permissions for an asset - pub async fn get_asset_permissions( - test_db: &TestDb, - asset_id: Uuid, - ) -> Result> { - // Implementation - } - - // Other permission helper methods -} -``` - -#### AssetTestHelpers - -The `AssetTestHelpers` struct provides utilities for creating and managing test assets: - -```rust -pub struct AssetTestHelpers; - -impl AssetTestHelpers { - /// Creates a test metric file - pub async fn create_test_metric( - test_db: &TestDb, - name: &str, - ) -> Result { - // Implementation - } - - /// Creates a test dashboard file - pub async fn create_test_dashboard( - test_db: &TestDb, - name: &str, - ) -> Result { - // Implementation - } - - /// Creates a test collection - pub async fn create_test_collection( - test_db: &TestDb, - name: &str, - ) -> Result { - // Implementation - } - - /// Creates a test chat - pub async fn create_test_chat( - test_db: &TestDb, - title: &str, - ) -> Result { - // Implementation - } - - /// Creates a test asset with permission in one step - pub async fn create_test_metric_with_permission( - test_db: &TestDb, - name: &str, - user_id: Uuid, - role: AssetPermissionRole, - ) -> Result { - // Implementation - } - - // Similar methods for other asset types with permissions -} -``` - -#### TestSetup - -The `TestSetup` struct combines the core utilities for easy test setup: - -```rust +// Helper struct for test setup pub struct TestSetup { pub user: AuthenticatedUser, pub organization: Organization, @@ -259,7 +187,6 @@ pub struct TestSetup { } impl TestSetup { - /// Creates a new test setup with authenticated user pub async fn new(role: Option) -> Result { let test_db = TestDb::new().await?; let (user, org) = test_db.create_authenticated_user(role).await?; diff --git a/api/libs/handlers/CLAUDE.md b/api/libs/handlers/CLAUDE.md index 10921cd80..18bf5f254 100644 --- a/api/libs/handlers/CLAUDE.md +++ b/api/libs/handlers/CLAUDE.md @@ -122,31 +122,53 @@ async fn example_handler(req: PostChatRequest, user: AuthenticatedUser) -> Resul - Run tests with: `cargo test -p handlers` - Create tests for each handler in the corresponding `tests/` directory -### Automatic Test Environment Setup +### Automatic Test Environment Setup and Best Practices -Integration tests in this library use an automatic database pool initialization system. The environment is set up once when the test module is loaded, eliminating the need for explicit initialization in each test. +Integration tests in this library benefit from an automatic database pool initialization system configured in `tests/mod.rs`. This setup uses `lazy_static` and `ctor` to initialize database pools (Postgres, Redis) once when the test module loads, meaning individual tests **do not** need to perform pool initialization. -**Important Notes:** -- This is a test-only feature that is excluded from release builds -- The test dependencies (`lazy_static` and `ctor`) are listed under `[dev-dependencies]` in Cargo.toml -- The entire `/tests` directory is only compiled during test runs (`cargo test`) +**Best Practice:** While you *can* directly use `get_pg_pool()` etc., it is **strongly recommended** to use the `TestSetup` or `TestDb` utilities from the `database` library's test commons (`database::tests::common::db`) for structuring your tests: -Key components: -- `tests/mod.rs` contains the initialization code using `lazy_static` and `ctor` -- Database pools are initialized only once for all tests -- Tests can directly use `get_pg_pool()` without any setup code +1. **Consistent Setup:** `TestSetup` provides a standard starting point with a pre-created test user, organization, and a `TestDb` instance. Use `TestDb` directly if you need more control over the initial setup. +2. **Helper Methods:** `TestDb` offers convenient methods for obtaining connections (`diesel_conn`, `sqlx_conn`, `redis_conn`) and creating common test entities (users, orgs, relationships) linked to the test instance. +3. **Crucial Cleanup:** `TestDb` includes a vital `cleanup()` method that removes data created during the test, ensuring test isolation. **This cleanup method MUST be called at the end of every test.** + +**Example Test Structure:** -Example test: ```rust use anyhow::Result; -use database::pool::get_pg_pool; +// Adjust import path based on actual visibility/re-exports +use database::tests::common::db::{TestSetup, TestDb}; +use database::enums::UserOrganizationRole; #[tokio::test] -async fn test_handler_functionality() -> Result<()> { - // Database pool is already initialized - let pool = get_pg_pool(); - - // Test code here using the pool +async fn test_handler_with_setup() -> Result<()> { + // 1. Initialize test environment using TestSetup + // This gives a user, org, and db instance. + let setup = TestSetup::new(Some(UserOrganizationRole::Member)).await?; + + // 2. Get connections via the db instance + let mut conn = setup.db.diesel_conn().await?; + + // 3. Use setup data (setup.user, setup.organization) and helpers (setup.db.create_...) + // Perform test logic... + // assert!(...) + + // 4. !!! Crucially, cleanup test data !!! + setup.db.cleanup().await?; + + Ok(()) +} + +#[tokio::test] +async fn test_handler_with_direct_db() -> Result<()> { + // Alternative: Use TestDb directly if no initial user/org needed + let db = TestDb::new().await?; + let mut conn = db.diesel_conn().await?; + + // Perform test logic... + + // !!! Crucially, cleanup test data !!! + db.cleanup().await?; Ok(()) } ``` @@ -154,4 +176,4 @@ async fn test_handler_functionality() -> Result<()> { To add new test modules, simply: 1. Create a new module in the `tests/` directory 2. Add it to the module declarations in `tests/mod.rs` -3. Write standard async tests using `#[tokio::test]` \ No newline at end of file +3. Write standard async tests using `#[tokio::test]` following the `TestSetup`/`TestDb` pattern above. \ No newline at end of file