//! Integration tests for `SqliteRelayLabelRepository`. //! //! These tests verify that the SQLite repository correctly: //! - Creates an in-memory database //! - Applies schema migrations //! - Validates table structure and constraints use sta::domain::relay::repository::RepositoryError; use sta::infrastructure::persistence::sqlite_repository::SqliteRelayLabelRepository; /// Test that `in_memory()` successfully creates an in-memory database. /// /// **T006 Requirement**: `SqliteRelayLabelRepository::in_memory()` creates in-memory DB with schema #[tokio::test] async fn test_in_memory_creates_database() { let result = SqliteRelayLabelRepository::in_memory().await; assert!( result.is_ok(), "Failed to create in-memory database: {:?}", result.err() ); } /// Test that the schema migration creates the `RelayLabels` table. /// /// **T006 Requirement**: Verify schema is applied correctly #[tokio::test] async fn test_in_memory_applies_schema() { let repo = SqliteRelayLabelRepository::in_memory() .await .expect("Failed to create in-memory database"); // Verify the table exists by querying it let result: Result<(String,), sqlx::Error> = sqlx::query_as("SELECT name FROM sqlite_master WHERE type='table' AND name='RelayLabels'") .fetch_one(repo.pool()) .await; assert!( result.is_ok(), "RelayLabels table should exist after migration" ); } /// Test that the `RelayLabels` table has the correct schema. /// /// **T006 Requirement**: Verify table structure matches migration #[tokio::test] async fn test_relay_labels_table_structure() { let repo = SqliteRelayLabelRepository::in_memory() .await .expect("Failed to create in-memory database"); // Query table info to verify column structure let columns: Vec<(String, String)> = sqlx::query_as("SELECT name, type FROM pragma_table_info('RelayLabels') ORDER BY cid") .fetch_all(repo.pool()) .await .expect("Failed to query table structure"); assert_eq!(columns.len(), 2, "RelayLabels table should have 2 columns"); // Verify relay_id column assert_eq!(columns[0].0, "relay_id", "First column should be relay_id"); assert_eq!(columns[0].1, "INTEGER", "relay_id should be INTEGER"); // Verify label column assert_eq!(columns[1].0, "label", "Second column should be label"); assert_eq!(columns[1].1, "TEXT", "label should be TEXT"); } /// Test that `relay_id` is the primary key. /// /// **T006 Requirement**: Verify primary key constraint #[tokio::test] async fn test_relay_id_primary_key() { let repo = SqliteRelayLabelRepository::in_memory() .await .expect("Failed to create in-memory database"); // Insert first row with relay_id = 1 let insert1: Result = sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (1, 'Test')") .execute(repo.pool()) .await; assert!(insert1.is_ok(), "First insert should succeed"); // Try to insert duplicate relay_id = 1 let insert2: Result = sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (1, 'Duplicate')") .execute(repo.pool()) .await; assert!( insert2.is_err(), "Duplicate relay_id should fail due to PRIMARY KEY constraint" ); } /// Test that `relay_id` must be between 1 and 8. /// /// **T006 Requirement**: Verify CHECK constraint on relay_id range #[tokio::test] async fn test_relay_id_range_constraint() { let repo = SqliteRelayLabelRepository::in_memory() .await .expect("Failed to create in-memory database"); // Valid range: 1-8 should succeed for id in 1..=8 { let result: Result = sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (?, ?)") .bind(id) .bind(format!("Relay {}", id)) .execute(repo.pool()) .await; assert!( result.is_ok(), "relay_id {} should be valid (range 1-8)", id ); } // Below valid range: 0 should fail let result_below: Result = sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (0, 'Invalid')") .execute(repo.pool()) .await; assert!( result_below.is_err(), "relay_id = 0 should fail CHECK constraint" ); // Above valid range: 9 should fail let result_above: Result = sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (9, 'Invalid')") .execute(repo.pool()) .await; assert!( result_above.is_err(), "relay_id = 9 should fail CHECK constraint" ); } /// Test that `label` cannot exceed 50 characters. /// /// **T006 Requirement**: Verify CHECK constraint on label length #[tokio::test] async fn test_label_length_constraint() { let repo = SqliteRelayLabelRepository::in_memory() .await .expect("Failed to create in-memory database"); // Valid length: 50 characters should succeed let label_50 = "A".repeat(50); let result_valid: Result = sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (1, ?)") .bind(&label_50) .execute(repo.pool()) .await; assert!( result_valid.is_ok(), "Label with 50 characters should be valid" ); // Invalid length: 51 characters should fail let label_51 = "B".repeat(51); let result_invalid: Result = sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (2, ?)") .bind(&label_51) .execute(repo.pool()) .await; assert!( result_invalid.is_err(), "Label with 51 characters should fail CHECK constraint" ); } /// Test that `label` cannot be NULL. /// /// **T006 Requirement**: Verify NOT NULL constraint on label #[tokio::test] async fn test_label_not_null_constraint() { let repo = SqliteRelayLabelRepository::in_memory() .await .expect("Failed to create in-memory database"); // Attempt to insert NULL label let result: Result = sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (1, NULL)") .execute(repo.pool()) .await; assert!( result.is_err(), "NULL label should fail NOT NULL constraint" ); } /// Test that multiple in-memory repositories are isolated. /// /// **T006 Requirement**: Verify in-memory instances are independent #[tokio::test] async fn test_multiple_in_memory_instances_isolated() { let repo1 = SqliteRelayLabelRepository::in_memory() .await .expect("Failed to create first in-memory database"); let repo2 = SqliteRelayLabelRepository::in_memory() .await .expect("Failed to create second in-memory database"); // Insert data into repo1 sqlx::query("INSERT INTO RelayLabels (relay_id, label) VALUES (1, 'Repo1')") .execute(repo1.pool()) .await .expect("Failed to insert into repo1"); // Verify repo2 is empty (no data from repo1) let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM RelayLabels") .fetch_one(repo2.pool()) .await .expect("Failed to query repo2"); assert_eq!( count.0, 0, "Second in-memory instance should be isolated from first" ); } /// Test that `new()` with file path creates a persistent database. /// /// **T006 Requirement**: Verify file-based database creation #[tokio::test] async fn test_new_creates_file_database() { let temp_db = tempfile::NamedTempFile::new().expect("Failed to create temp file"); let db_path = format!("sqlite://{}", temp_db.path().to_str().unwrap()); let result = SqliteRelayLabelRepository::new(&db_path).await; assert!( result.is_ok(), "Failed to create file-based database: {:?}", result.err() ); // Verify the file exists and has content let metadata = std::fs::metadata(temp_db.path()).expect("Database file should exist"); assert!(metadata.len() > 0, "Database file should not be empty"); } /// Test that `new()` with invalid path returns error. /// /// **T006 Requirement**: Verify error handling for invalid paths #[tokio::test] async fn test_new_invalid_path_returns_error() { let result = SqliteRelayLabelRepository::new("sqlite:///invalid/path/that/does/not/exist/db.sqlite") .await; assert!(result.is_err(), "Invalid database path should return error"); match result { Err(RepositoryError::DatabaseError(_)) => { // Expected error type } _ => panic!("Expected RepositoryError::DatabaseError for invalid path"), } }