Files
sta/backend/tests/sqlite_repository_test.rs

267 lines
9.0 KiB
Rust
Raw Normal View History

//! 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::sqlite::SqliteQueryResult, sqlx::Error> =
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::sqlite::SqliteQueryResult, sqlx::Error> =
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::sqlite::SqliteQueryResult, sqlx::Error> =
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::sqlite::SqliteQueryResult, sqlx::Error> =
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::sqlite::SqliteQueryResult, sqlx::Error> =
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::sqlite::SqliteQueryResult, sqlx::Error> =
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::sqlite::SqliteQueryResult, sqlx::Error> =
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::sqlite::SqliteQueryResult, sqlx::Error> =
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"),
}
}