feat: wire relay API with dependency injection
- split settings module into per-struct files
- add DatabaseSettings with default in-memory SQLite path
- implement RelayApi struct with GET /relays and POST
/relays/{id}/toggle
- wire create_relay_controller and create_label_repository into
Application::build() with mock/real selection via cfg!(test) || CI
- register RelayApi in OpenApiService alongside existing APIs
This commit is contained in:
@@ -5,13 +5,15 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::domain::relay::repository::{RelayLabelRepository, RepositoryError};
|
||||
use crate::{domain::relay::repository::{RelayLabelRepository, RepositoryError}, infrastructure::persistence::label_repository::MockRelayLabelRepository};
|
||||
|
||||
use super::sqlite_repository::SqliteRelayLabelRepository;
|
||||
|
||||
/// Creates a relay label repository based on configuration.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// - `db_path`: Path to SQLite database file (e.g., "relays.db" or ":memory:")
|
||||
/// - `db_path`: Path to ``SQLite`` database file (e.g., "relays.db" or ":memory:")
|
||||
/// - `use_mock`: If true, returns `MockRelayLabelRepository` for testing
|
||||
///
|
||||
/// # Returns
|
||||
@@ -23,14 +25,18 @@ use crate::domain::relay::repository::{RelayLabelRepository, RepositoryError};
|
||||
///
|
||||
/// Returns `RepositoryError` if:
|
||||
/// - Database path is invalid or inaccessible
|
||||
/// - SQLite connection fails
|
||||
/// - ``SQLite`` connection fails
|
||||
/// - Database schema migration fails
|
||||
pub fn create_label_repository(
|
||||
_db_path: &str,
|
||||
_use_mock: bool,
|
||||
pub async fn create_label_repository(
|
||||
db_path: &str,
|
||||
use_mock: bool,
|
||||
) -> Result<Arc<dyn RelayLabelRepository>, RepositoryError> {
|
||||
// TODO: Implement in T039b
|
||||
unimplemented!("T039b: create_label_repository factory not yet implemented")
|
||||
if use_mock {
|
||||
tracing::info!("Using MockRelayLabelRepository (test mode)");
|
||||
return Ok(Arc::new(MockRelayLabelRepository::new()));
|
||||
}
|
||||
let repo = SqliteRelayLabelRepository::new(db_path).await?;
|
||||
Ok(Arc::new(repo))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -38,28 +44,14 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::domain::relay::types::{RelayId, RelayLabel};
|
||||
|
||||
// T039b: Test 1 - use_mock=true returns MockLabelRepository
|
||||
#[tokio::test]
|
||||
async fn test_create_label_repository_with_mock_flag() {
|
||||
// GIVEN: use_mock=true (db_path is ignored in mock mode)
|
||||
let db_path = ":memory:";
|
||||
|
||||
// WHEN: create_label_repository is called with use_mock=true
|
||||
let result = create_label_repository(db_path, true);
|
||||
|
||||
// THEN: Should return MockLabelRepository successfully
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to create mock repository"
|
||||
);
|
||||
|
||||
let result = create_label_repository(db_path, true).await;
|
||||
assert!(result.is_ok(), "Failed to create mock repository");
|
||||
let repository = result.unwrap();
|
||||
|
||||
// Verify it's a mock by testing basic operations
|
||||
// Mock repository should start empty
|
||||
let relay_id = RelayId::new(1).unwrap();
|
||||
let label_result = repository.get_label(relay_id).await;
|
||||
|
||||
assert!(
|
||||
label_result.is_ok(),
|
||||
"Mock repository should be immediately usable"
|
||||
@@ -71,56 +63,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// T039b: Test 2 - use_mock=false returns SqliteRelayLabelRepository
|
||||
#[tokio::test]
|
||||
async fn test_create_label_repository_with_sqlite() {
|
||||
// GIVEN: Valid in-memory SQLite database path
|
||||
let db_path = ":memory:";
|
||||
|
||||
// WHEN: create_label_repository is called with use_mock=false
|
||||
let result = create_label_repository(db_path, false);
|
||||
|
||||
// THEN: Should return SqliteRelayLabelRepository successfully
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to create SQLite repository"
|
||||
);
|
||||
|
||||
let result = create_label_repository(db_path, false).await;
|
||||
assert!(result.is_ok(), "Failed to create SQLite repository");
|
||||
let repository = result.unwrap();
|
||||
|
||||
// Verify it's working by performing a basic operation
|
||||
let relay_id = RelayId::new(1).unwrap();
|
||||
let label = RelayLabel::new("Pump".to_string()).unwrap();
|
||||
|
||||
// Should be able to save and get labels
|
||||
let save_result = repository.save_label(relay_id, label.clone()).await;
|
||||
assert!(
|
||||
save_result.is_ok(),
|
||||
"Failed to save label on SQLite repository"
|
||||
);
|
||||
|
||||
let get_result = repository.get_label(relay_id).await;
|
||||
assert!(get_result.is_ok(), "Failed to get label");
|
||||
assert_eq!(get_result.unwrap(), Some(label));
|
||||
}
|
||||
|
||||
// T039b: Test 3 - Invalid db_path returns RepositoryError
|
||||
#[test]
|
||||
fn test_create_label_repository_with_invalid_path() {
|
||||
// GIVEN: Invalid database path (directory that doesn't exist)
|
||||
#[tokio::test]
|
||||
async fn test_create_label_repository_with_invalid_path() {
|
||||
let db_path = "/nonexistent/directory/impossible/path/relays.db";
|
||||
|
||||
// WHEN: create_label_repository is called with use_mock=false
|
||||
let result = create_label_repository(db_path, false);
|
||||
|
||||
// THEN: Should return RepositoryError
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Should fail with invalid database path"
|
||||
);
|
||||
|
||||
// Verify the error is appropriate
|
||||
let result = create_label_repository(db_path, false).await;
|
||||
assert!(result.is_err(), "Should fail with invalid database path");
|
||||
if let Err(error) = result {
|
||||
#[allow(clippy::match_wildcard_for_single_variants)]
|
||||
match error {
|
||||
RepositoryError::DatabaseError(_) => {
|
||||
// Expected error type - test passes
|
||||
@@ -130,20 +97,13 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// Additional test: Verify mock and SQLite repositories are independent
|
||||
#[tokio::test]
|
||||
async fn test_mock_and_sqlite_repositories_are_independent() {
|
||||
// GIVEN: Both mock and SQLite repositories
|
||||
let mock_repo = create_label_repository(":memory:", true).unwrap();
|
||||
let sqlite_repo = create_label_repository(":memory:", false).unwrap();
|
||||
|
||||
let mock_repo = create_label_repository(":memory:", true).await.unwrap();
|
||||
let sqlite_repo = create_label_repository(":memory:", false).await.unwrap();
|
||||
let relay_id = RelayId::new(1).unwrap();
|
||||
let label = RelayLabel::new("Test".to_string()).unwrap();
|
||||
|
||||
// WHEN: We save a label in the mock repository
|
||||
mock_repo.save_label(relay_id, label.clone()).await.unwrap();
|
||||
|
||||
// THEN: The SQLite repository should not have that label
|
||||
let sqlite_result = sqlite_repo.get_label(relay_id).await.unwrap();
|
||||
assert_eq!(
|
||||
sqlite_result, None,
|
||||
@@ -151,23 +111,15 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// Additional test: Verify in-memory SQLite doesn't persist
|
||||
#[tokio::test]
|
||||
async fn test_in_memory_sqlite_does_not_persist() {
|
||||
// GIVEN: An in-memory SQLite database
|
||||
let relay_id = RelayId::new(1).unwrap();
|
||||
let label = RelayLabel::new("Temporary".to_string()).unwrap();
|
||||
|
||||
// WHEN: We create a repository, save a label, and drop it
|
||||
{
|
||||
let repo = create_label_repository(":memory:", false).unwrap();
|
||||
let repo = create_label_repository(":memory:", false).await.unwrap();
|
||||
repo.save_label(relay_id, label.clone()).await.unwrap();
|
||||
} // repo is dropped here
|
||||
|
||||
// AND: We create a new in-memory repository
|
||||
let new_repo = create_label_repository(":memory:", false).unwrap();
|
||||
|
||||
// THEN: The label should not exist in the new repository
|
||||
let new_repo = create_label_repository(":memory:", false).await.unwrap();
|
||||
let result = new_repo.get_label(relay_id).await.unwrap();
|
||||
assert_eq!(
|
||||
result, None,
|
||||
|
||||
@@ -12,11 +12,13 @@
|
||||
|
||||
#[cfg(test)]
|
||||
mod relay_label_repository_contract_tests {
|
||||
use crate::{domain::relay::{
|
||||
repository::RelayLabelRepository,
|
||||
types::{RelayId, RelayLabel},
|
||||
}, infrastructure::persistence::label_repository::MockRelayLabelRepository};
|
||||
|
||||
use crate::{
|
||||
domain::relay::{
|
||||
repository::RelayLabelRepository,
|
||||
types::{RelayId, RelayLabel},
|
||||
},
|
||||
infrastructure::persistence::label_repository::MockRelayLabelRepository,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_get_label_returns_none_for_non_existent_relay() {
|
||||
@@ -75,7 +77,6 @@ mod relay_label_repository_contract_tests {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_save_label_succeeds() {
|
||||
let repo = MockRelayLabelRepository::new();
|
||||
@@ -179,7 +180,6 @@ mod relay_label_repository_contract_tests {
|
||||
assert_eq!(retrieved.unwrap().as_str(), "X", "Label should match");
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_delete_label_succeeds_for_existing_label() {
|
||||
let repo = MockRelayLabelRepository::new();
|
||||
@@ -265,7 +265,6 @@ mod relay_label_repository_contract_tests {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_get_all_labels_returns_empty_when_no_labels() {
|
||||
let repo = MockRelayLabelRepository::new();
|
||||
|
||||
Reference in New Issue
Block a user