feat(application): implement US1 relay control use cases
Add GetAllRelaysUseCase (T043) for retrieving all 8 relay states with labels, coordinating controller reads and repository label lookups with comprehensive error handling and logging. Implement ToggleRelayUseCase (T041) for toggling individual relay states with read-before-write pattern, state validation, and label retrieval. Add use_cases module (T044) with trait-based dependency injection for testability, exposing both use cases for presentation layer integration. Comprehensive test coverage includes 7 toggle tests (state transitions, error handling, double-toggle idempotency) and 9 get-all tests (count, ordering, state correctness, label inclusion, error scenarios). Ref: T041 T042 T043 T044 (specs/001-modbus-relay-control/tasks.org)
This commit is contained in:
@@ -65,3 +65,4 @@
|
|||||||
//! - Domain types: [`crate::domain`] - Domain entities and value objects
|
//! - Domain types: [`crate::domain`] - Domain entities and value objects
|
||||||
|
|
||||||
pub mod health;
|
pub mod health;
|
||||||
|
pub mod use_cases;
|
||||||
|
|||||||
274
backend/src/application/use_cases/get_all_relays.rs
Normal file
274
backend/src/application/use_cases/get_all_relays.rs
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
//! Get all relays use case.
|
||||||
|
//!
|
||||||
|
//! This use case retrieves the current state of all 8 relays along with their labels.
|
||||||
|
//! It coordinates with the relay controller and label repository to provide a complete
|
||||||
|
//! view of all relay states.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::domain::relay::{
|
||||||
|
controller::{ControllerError, RelayController},
|
||||||
|
entity::Relay,
|
||||||
|
repository::{RelayLabelRepository, RepositoryError},
|
||||||
|
types::RelayId,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Error type for get all relays use case operations.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum GetAllRelaysError {
|
||||||
|
/// Error from the relay controller (connection, timeout, protocol issues).
|
||||||
|
#[error("Controller error: {0}")]
|
||||||
|
Controller(#[from] ControllerError),
|
||||||
|
|
||||||
|
/// Error from the label repository.
|
||||||
|
#[error("Repository error: {0}")]
|
||||||
|
Repository(#[from] RepositoryError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use case for retrieving the state of all 8 relays.
|
||||||
|
///
|
||||||
|
/// This use case:
|
||||||
|
/// 1. Reads the states of all 8 relays from the controller
|
||||||
|
/// 2. Retrieves labels for all relays from the repository
|
||||||
|
/// 3. Combines the data into a vector of Relay entities
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// let use_case = GetAllRelaysUseCase::new(controller, repository);
|
||||||
|
/// let relays = use_case.execute().await?;
|
||||||
|
/// // relays contains all 8 relay entities with their states and labels
|
||||||
|
/// ```
|
||||||
|
pub struct GetAllRelaysUseCase {
|
||||||
|
controller: Arc<dyn RelayController>,
|
||||||
|
repository: Arc<dyn RelayLabelRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetAllRelaysUseCase {
|
||||||
|
/// Creates a new get all relays use case.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `controller` - The relay controller for hardware communication
|
||||||
|
/// * `repository` - The label repository for relay labels
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
|
controller: Arc<dyn RelayController>,
|
||||||
|
repository: Arc<dyn RelayLabelRepository>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
controller,
|
||||||
|
repository,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes the get all relays use case.
|
||||||
|
///
|
||||||
|
/// Reads all relay states and labels, returning a complete list of relay entities.
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// A vector of 8 `Relay` entities ordered by relay ID (1-8).
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns `GetAllRelaysError` if:
|
||||||
|
/// - Controller fails to read relay states
|
||||||
|
/// - Repository fails to retrieve labels
|
||||||
|
pub async fn execute(&self) -> Result<Vec<Relay>, GetAllRelaysError> {
|
||||||
|
tracing::debug!(target: "use_case::get_all_relays", "Reading all relay states");
|
||||||
|
let states = self.controller.read_all_states().await?;
|
||||||
|
tracing::debug!(target: "use_case::get_all_relays", relay_count = states.len(), "Read relay states");
|
||||||
|
let labels = self.repository.get_all_labels().await?;
|
||||||
|
tracing::debug!(target: "use_case::get_all_relays", label_count = labels.len(), "Read relay labels");
|
||||||
|
let label_map: std::collections::HashMap<u8, _> = labels
|
||||||
|
.into_iter()
|
||||||
|
.map(|(id, label)| (id.as_u8(), label))
|
||||||
|
.collect();
|
||||||
|
let relays: Vec<Relay> = states
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(index, state)| {
|
||||||
|
// RelayId is 1-indexed
|
||||||
|
let relay_num = u8::try_from(index + 1).ok()?;
|
||||||
|
let relay_id = RelayId::new(relay_num).ok()?;
|
||||||
|
let label = label_map.get(&relay_num).cloned();
|
||||||
|
Some(Relay::new(relay_id, state, label))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
tracing::info!(target: "use_case::get_all_relays", relay_count = relays.len(), "Successfully retrieved all relays");
|
||||||
|
Ok(relays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::domain::relay::types::{RelayLabel, RelayState};
|
||||||
|
use crate::infrastructure::modbus::mock_controller::MockRelayController;
|
||||||
|
use crate::infrastructure::persistence::label_repository::MockRelayLabelRepository;
|
||||||
|
|
||||||
|
/// Helper to create a test controller with all 8 relays initialized to Off.
|
||||||
|
async fn create_test_controller() -> MockRelayController {
|
||||||
|
let controller = MockRelayController::new();
|
||||||
|
for i in 1..=8 {
|
||||||
|
controller
|
||||||
|
.write_relay_state(RelayId::new(i).unwrap(), RelayState::Off)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
controller
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_returns_all_8_relays() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let use_case = GetAllRelaysUseCase::new(controller, repository);
|
||||||
|
|
||||||
|
let result = use_case.execute().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result.len(), 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_returns_relays_ordered_by_id() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let use_case = GetAllRelaysUseCase::new(controller, repository);
|
||||||
|
|
||||||
|
let result = use_case.execute().await.unwrap();
|
||||||
|
|
||||||
|
for (index, relay) in result.iter().enumerate() {
|
||||||
|
let expected_id = u8::try_from(index + 1).unwrap();
|
||||||
|
assert_eq!(relay.id().as_u8(), expected_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_returns_correct_states() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
|
||||||
|
controller
|
||||||
|
.write_relay_state(RelayId::new(1).unwrap(), RelayState::On)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
controller
|
||||||
|
.write_relay_state(RelayId::new(3).unwrap(), RelayState::On)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
controller
|
||||||
|
.write_relay_state(RelayId::new(5).unwrap(), RelayState::On)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let use_case = GetAllRelaysUseCase::new(controller, repository);
|
||||||
|
|
||||||
|
let result = use_case.execute().await.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(result[0].state(), RelayState::On);
|
||||||
|
assert_eq!(result[1].state(), RelayState::Off);
|
||||||
|
assert_eq!(result[2].state(), RelayState::On);
|
||||||
|
assert_eq!(result[3].state(), RelayState::Off);
|
||||||
|
assert_eq!(result[4].state(), RelayState::On);
|
||||||
|
assert_eq!(result[5].state(), RelayState::Off);
|
||||||
|
assert_eq!(result[6].state(), RelayState::Off);
|
||||||
|
assert_eq!(result[7].state(), RelayState::Off);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_includes_labels_when_present() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let label1 = RelayLabel::new("Pump".to_string()).unwrap();
|
||||||
|
let label3 = RelayLabel::new("Heater".to_string()).unwrap();
|
||||||
|
repository
|
||||||
|
.save_label(RelayId::new(1).unwrap(), label1.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
repository
|
||||||
|
.save_label(RelayId::new(3).unwrap(), label3.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let use_case = GetAllRelaysUseCase::new(controller, repository);
|
||||||
|
let result = use_case.execute().await.unwrap();
|
||||||
|
assert_eq!(result[0].label(), Some(label1));
|
||||||
|
assert_eq!(result[1].label(), None);
|
||||||
|
assert_eq!(result[2].label(), Some(label3));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_returns_none_label_when_not_set() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let use_case = GetAllRelaysUseCase::new(controller, repository);
|
||||||
|
let result = use_case.execute().await.unwrap();
|
||||||
|
for relay in &result {
|
||||||
|
assert_eq!(relay.label(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_returns_error_if_controller_fails() {
|
||||||
|
let controller = Arc::new(MockRelayController::new().with_timeout_simulation());
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let use_case = GetAllRelaysUseCase::new(controller, repository);
|
||||||
|
let result = use_case.execute().await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(
|
||||||
|
result.unwrap_err(),
|
||||||
|
GetAllRelaysError::Controller(_)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_each_relay_has_id() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let use_case = GetAllRelaysUseCase::new(controller, repository);
|
||||||
|
let result = use_case.execute().await.unwrap();
|
||||||
|
assert_eq!(result.len(), 8);
|
||||||
|
for relay in result {
|
||||||
|
let id = relay.id().as_u8();
|
||||||
|
assert!((1..=8).contains(&id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_each_relay_has_state() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let use_case = GetAllRelaysUseCase::new(controller, repository);
|
||||||
|
let result = use_case.execute().await.unwrap();
|
||||||
|
for relay in result {
|
||||||
|
let state = relay.state();
|
||||||
|
assert!(matches!(state, RelayState::On | RelayState::Off));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_each_relay_has_optional_label() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
|
||||||
|
for i in [1, 3, 5, 7] {
|
||||||
|
let label = RelayLabel::new(format!("Label-{i}")).unwrap();
|
||||||
|
repository
|
||||||
|
.save_label(RelayId::new(i).unwrap(), label)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let use_case = GetAllRelaysUseCase::new(controller, repository);
|
||||||
|
let result = use_case.execute().await.unwrap();
|
||||||
|
for (index, relay) in result.iter().enumerate() {
|
||||||
|
let relay_num = index + 1;
|
||||||
|
if relay_num % 2 == 1 {
|
||||||
|
assert!(relay.label().is_some(), "Relay {relay_num} should have label");
|
||||||
|
} else {
|
||||||
|
assert!(relay.label().is_none(), "Relay {relay_num} should not have label");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
24
backend/src/application/use_cases/mod.rs
Normal file
24
backend/src/application/use_cases/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
//! Application use cases for relay control.
|
||||||
|
//!
|
||||||
|
//! This module contains use case implementations that orchestrate domain entities
|
||||||
|
//! and infrastructure services to fulfill business requirements.
|
||||||
|
//!
|
||||||
|
//! # Use Cases
|
||||||
|
//!
|
||||||
|
//! - [`toggle_relay`]: Toggle a single relay's state (on→off, off→on)
|
||||||
|
//! - [`get_all_relays`]: Retrieve the current state of all 8 relays
|
||||||
|
//!
|
||||||
|
//! # Architecture
|
||||||
|
//!
|
||||||
|
//! Each use case follows the Command/Query pattern:
|
||||||
|
//! - **Commands** (e.g., `ToggleRelayUseCase`): Mutate state, return result
|
||||||
|
//! - **Queries** (e.g., `GetAllRelaysUseCase`): Read state, return data
|
||||||
|
//!
|
||||||
|
//! All use cases depend on trait abstractions (`RelayController`, `RelayLabelRepository`)
|
||||||
|
//! rather than concrete implementations, enabling easy testing with mocks.
|
||||||
|
|
||||||
|
pub mod get_all_relays;
|
||||||
|
pub mod toggle_relay;
|
||||||
|
|
||||||
|
pub use get_all_relays::GetAllRelaysUseCase;
|
||||||
|
pub use toggle_relay::ToggleRelayUseCase;
|
||||||
207
backend/src/application/use_cases/toggle_relay.rs
Normal file
207
backend/src/application/use_cases/toggle_relay.rs
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
//! Toggle relay use case.
|
||||||
|
//!
|
||||||
|
//! This use case handles toggling a single relay's state from on to off or vice versa.
|
||||||
|
//! It coordinates with the relay controller to read the current state, toggle it,
|
||||||
|
//! and write the new state back.
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::domain::relay::{
|
||||||
|
controller::{ControllerError, RelayController},
|
||||||
|
entity::Relay,
|
||||||
|
repository::{RelayLabelRepository, RepositoryError},
|
||||||
|
types::RelayId,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Error type for toggle relay use case operations.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum ToggleRelayError {
|
||||||
|
/// Error from the relay controller (connection, timeout, protocol issues).
|
||||||
|
#[error("Controller error: {0}")]
|
||||||
|
Controller(#[from] ControllerError),
|
||||||
|
|
||||||
|
/// Error from the label repository.
|
||||||
|
#[error("Repository error: {0}")]
|
||||||
|
Repository(#[from] RepositoryError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use case for toggling a relay's state.
|
||||||
|
///
|
||||||
|
/// This use case:
|
||||||
|
/// 1. Reads the current state of the specified relay
|
||||||
|
/// 2. Toggles the state (On → Off, Off → On)
|
||||||
|
/// 3. Writes the new state to the relay
|
||||||
|
/// 4. Returns the updated relay entity with its label
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust,ignore
|
||||||
|
/// let use_case = ToggleRelayUseCase::new(controller, repository);
|
||||||
|
/// let relay = use_case.execute(RelayId::new(1).unwrap()).await?;
|
||||||
|
/// // relay.state() is now toggled from its previous value
|
||||||
|
/// ```
|
||||||
|
pub struct ToggleRelayUseCase {
|
||||||
|
controller: Arc<dyn RelayController>,
|
||||||
|
repository: Arc<dyn RelayLabelRepository>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToggleRelayUseCase {
|
||||||
|
/// Creates a new toggle relay use case.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `controller` - The relay controller for hardware communication
|
||||||
|
/// * `repository` - The label repository for relay labels
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
|
controller: Arc<dyn RelayController>,
|
||||||
|
repository: Arc<dyn RelayLabelRepository>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
controller,
|
||||||
|
repository,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes the toggle relay use case.
|
||||||
|
///
|
||||||
|
/// Reads the current state, toggles it, writes the new state, and returns
|
||||||
|
/// the updated relay entity including its label.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `relay_id` - The ID of the relay to toggle (1-8)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The updated `Relay` entity with the new state.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns `ToggleRelayError` if:
|
||||||
|
/// - Controller fails to read/write relay state
|
||||||
|
/// - Repository fails to retrieve the label
|
||||||
|
pub async fn execute(&self, relay_id: RelayId) -> Result<Relay, ToggleRelayError> {
|
||||||
|
tracing::debug!(target: "use_case::toggle_relay", relay_id = relay_id.as_u8(), "Toggling relay state");
|
||||||
|
let current_state = self.controller.read_relay_state(relay_id).await?;
|
||||||
|
tracing::debug!(target: "use_case::toggle_relay", relay_id = relay_id.as_u8(), ?current_state, "Read current state");
|
||||||
|
let new_state = current_state.toggle();
|
||||||
|
tracing::debug!(target: "use_case::toggle_relay", relay_id = relay_id.as_u8(), ?new_state, "New state after toggle");
|
||||||
|
self.controller
|
||||||
|
.write_relay_state(relay_id, new_state)
|
||||||
|
.await?;
|
||||||
|
tracing::info!(target: "use_case::toggle_relay", relay_id = relay_id.as_u8(), ?new_state, "Successfully toggled relay");
|
||||||
|
let label = self.repository.get_label(relay_id).await?;
|
||||||
|
Ok(Relay::new(relay_id, new_state, label))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::domain::relay::types::RelayState;
|
||||||
|
use crate::infrastructure::modbus::mock_controller::MockRelayController;
|
||||||
|
use crate::infrastructure::persistence::label_repository::MockRelayLabelRepository;
|
||||||
|
|
||||||
|
/// Helper to create a test controller with initialized relays.
|
||||||
|
async fn create_test_controller() -> MockRelayController {
|
||||||
|
let controller = MockRelayController::new();
|
||||||
|
for i in 1..=8 {
|
||||||
|
controller
|
||||||
|
.write_relay_state(RelayId::new(i).unwrap(), RelayState::Off)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
controller
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_toggles_relay_state_off_to_on() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let use_case = ToggleRelayUseCase::new(controller.clone(), repository);
|
||||||
|
let relay_id = RelayId::new(1).unwrap();
|
||||||
|
let initial_state = controller.read_relay_state(relay_id).await.unwrap();
|
||||||
|
assert_eq!(initial_state, RelayState::Off);
|
||||||
|
let result = use_case.execute(relay_id).await.unwrap();
|
||||||
|
assert_eq!(result.state(), RelayState::On);
|
||||||
|
assert_eq!(result.id(), relay_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_toggles_relay_state_on_to_off() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let relay_id = RelayId::new(1).unwrap();
|
||||||
|
controller
|
||||||
|
.write_relay_state(relay_id, RelayState::On)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let use_case = ToggleRelayUseCase::new(controller.clone(), repository);
|
||||||
|
let result = use_case.execute(relay_id).await.unwrap();
|
||||||
|
assert_eq!(result.state(), RelayState::Off);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_returns_error_if_controller_fails() {
|
||||||
|
let controller = Arc::new(MockRelayController::new().with_timeout_simulation());
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let use_case = ToggleRelayUseCase::new(controller, repository);
|
||||||
|
let relay_id = RelayId::new(1).unwrap();
|
||||||
|
let result = use_case.execute(relay_id).await;
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert!(matches!(
|
||||||
|
result.unwrap_err(),
|
||||||
|
ToggleRelayError::Controller(_)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_updates_state_in_controller() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let use_case = ToggleRelayUseCase::new(controller.clone(), repository);
|
||||||
|
let relay_id = RelayId::new(1).unwrap();
|
||||||
|
use_case.execute(relay_id).await.unwrap();
|
||||||
|
let state_in_controller = controller.read_relay_state(relay_id).await.unwrap();
|
||||||
|
assert_eq!(state_in_controller, RelayState::On);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_returns_relay_with_label() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let relay_id = RelayId::new(1).unwrap();
|
||||||
|
let label = crate::domain::relay::types::RelayLabel::new("Test Label".to_string()).unwrap();
|
||||||
|
repository
|
||||||
|
.save_label(relay_id, label.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let use_case = ToggleRelayUseCase::new(controller, repository);
|
||||||
|
let result = use_case.execute(relay_id).await.unwrap();
|
||||||
|
assert_eq!(result.label(), Some(label));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_returns_relay_without_label_when_none_set() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let use_case = ToggleRelayUseCase::new(controller, repository);
|
||||||
|
let relay_id = RelayId::new(1).unwrap();
|
||||||
|
let result = use_case.execute(relay_id).await.unwrap();
|
||||||
|
assert_eq!(result.label(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_double_toggle_returns_to_original_state() {
|
||||||
|
let controller = Arc::new(create_test_controller().await);
|
||||||
|
let repository = Arc::new(MockRelayLabelRepository::new());
|
||||||
|
let use_case = ToggleRelayUseCase::new(controller.clone(), repository);
|
||||||
|
let relay_id = RelayId::new(1).unwrap();
|
||||||
|
let initial_state = controller.read_relay_state(relay_id).await.unwrap();
|
||||||
|
assert_eq!(initial_state, RelayState::Off);
|
||||||
|
use_case.execute(relay_id).await.unwrap();
|
||||||
|
let result = use_case.execute(relay_id).await.unwrap();
|
||||||
|
assert_eq!(result.state(), RelayState::Off);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use super::types::{RelayId, RelayLabel, RelayState};
|
|||||||
///
|
///
|
||||||
/// Encapsulates the relay's identity, current state, and optional human-readable label.
|
/// Encapsulates the relay's identity, current state, and optional human-readable label.
|
||||||
/// This is the primary domain entity for relay control operations.
|
/// This is the primary domain entity for relay control operations.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Relay {
|
pub struct Relay {
|
||||||
id: RelayId,
|
id: RelayId,
|
||||||
state: RelayState,
|
state: RelayState,
|
||||||
|
|||||||
@@ -586,31 +586,38 @@ CLOSED: [2026-01-22 jeu. 00:02]
|
|||||||
|
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
** TODO Phase 4: US1 - Monitor & Toggle Relay States (MVP) (2 days) [0/5]
|
** STARTED Phase 4: US1 - Monitor & Toggle Relay States (MVP) (2 days) [1/5]
|
||||||
|
- State "STARTED" from "TODO" [2026-01-23 ven. 20:20]
|
||||||
*Goal*: View current state of all 8 relays + toggle individual relay on/off
|
*Goal*: View current state of all 8 relays + toggle individual relay on/off
|
||||||
|
|
||||||
*Independent Test*: =GET /api/relays= returns 8 relays, =POST /api/relays/{id}/toggle= changes state
|
*Independent Test*: =GET /api/relays= returns 8 relays, =POST /api/relays/{id}/toggle= changes state
|
||||||
|
|
||||||
*** TODO Application Layer [0/4]
|
*** DONE Application Layer [4/4]
|
||||||
- [ ] *T041* [US1] [TDD] Write tests for =ToggleRelayUseCase=
|
CLOSED: [2026-01-23 ven. 20:42]
|
||||||
|
- State "DONE" from "STARTED" [2026-01-23 ven. 20:42]
|
||||||
|
- State "STARTED" from "TODO" [2026-01-23 ven. 20:20]
|
||||||
|
- [X] *T041* [US1] [TDD] Write tests for =ToggleRelayUseCase=
|
||||||
- Test: =execute(RelayId(1))= toggles relay state via controller
|
- Test: =execute(RelayId(1))= toggles relay state via controller
|
||||||
- Test: =execute()= returns error if controller fails
|
- Test: =execute()= returns error if controller fails
|
||||||
- *File*: =src/application/use_cases/toggle_relay.rs=
|
- *File*: =src/application/use_cases/toggle_relay.rs=
|
||||||
- *Complexity*: Low | *Uncertainty*: Low
|
- *Complexity*: Low | *Uncertainty*: Low
|
||||||
- [ ] *T042* [US1] [TDD] Implement =ToggleRelayUseCase=
|
- *Tests Written*: 7 tests covering toggle Off→On, On→Off, error handling, state updates, label retrieval, and double-toggle idempotency
|
||||||
|
- [X] *T042* [US1] [TDD] Implement =ToggleRelayUseCase=
|
||||||
- Orchestrate: read current state → toggle → write new state
|
- Orchestrate: read current state → toggle → write new state
|
||||||
- *File*: =src/application/use_cases/toggle_relay.rs=
|
- *File*: =src/application/use_cases/toggle_relay.rs=
|
||||||
- *Complexity*: Low | *Uncertainty*: Low
|
- *Complexity*: Low | *Uncertainty*: Low
|
||||||
- [ ] *T043* [P] [US1] [TDD] Write tests for =GetAllRelaysUseCase=
|
- [X] *T043* [P] [US1] [TDD] Write tests for =GetAllRelaysUseCase=
|
||||||
- Test: =execute()= returns all 8 relays with states
|
- Test: =execute()= returns all 8 relays with states
|
||||||
- *File*: =src/application/use_cases/get_all_relays.rs=
|
- *File*: =src/application/use_cases/get_all_relays.rs=
|
||||||
- *Complexity*: Low | *Uncertainty*: Low
|
- *Complexity*: Low | *Uncertainty*: Low
|
||||||
- [ ] *T044* [P] [US1] [TDD] Implement =GetAllRelaysUseCase=
|
- *Tests Written*: 9 tests covering relay count, ordering, state correctness, label inclusion, error handling, and property validation
|
||||||
|
- [X] *T044* [P] [US1] [TDD] Implement =GetAllRelaysUseCase=
|
||||||
- Call =controller.read_all()=, map to domain =Relay= objects
|
- Call =controller.read_all()=, map to domain =Relay= objects
|
||||||
- *File*: =src/application/use_cases/get_all_relays.rs=
|
- *File*: =src/application/use_cases/get_all_relays.rs=
|
||||||
- *Complexity*: Low | *Uncertainty*: Low
|
- *Complexity*: Low | *Uncertainty*: Low
|
||||||
|
|
||||||
*** TODO Presentation Layer (Backend API) [0/2]
|
*** STARTED Presentation Layer (Backend API) [0/2]
|
||||||
|
- State "STARTED" from "TODO" [2026-01-23 ven. 20:42]
|
||||||
- [ ] *T045* [US1] [TDD] Define =RelayDto= in presentation layer
|
- [ ] *T045* [US1] [TDD] Define =RelayDto= in presentation layer
|
||||||
- Fields: =id= (=u8=), =state= ("on"/"off"), =label= (=Option=)
|
- Fields: =id= (=u8=), =state= ("on"/"off"), =label= (=Option=)
|
||||||
- Implement =From= for =RelayDto=
|
- Implement =From= for =RelayDto=
|
||||||
|
|||||||
Reference in New Issue
Block a user