//! Mock implementation and tests for `RelayLabelRepository` trait. //! //! This module provides a simple in-memory mock implementation of the //! `RelayLabelRepository` trait for testing purposes, along with comprehensive //! tests that verify the trait's contract. use std::collections::HashMap; use std::sync::Arc; use async_trait::async_trait; use tokio::sync::{Mutex, MutexGuard}; use crate::domain::relay::{ repository::{RelayLabelRepository, RepositoryError}, types::{RelayId, RelayLabel}, }; /// In-memory mock implementation of `RelayLabelRepository` for testing. /// /// This implementation uses a `HashMap` wrapped in `Arc>` to provide /// thread-safe concurrent access to relay labels. It's useful for testing /// application logic without requiring a database connection. #[derive(Debug, Clone)] pub struct MockRelayLabelRepository { /// Internal storage for relay labels, protected by a mutex for thread safety. labels: Arc>>, } impl MockRelayLabelRepository { /// Creates a new empty mock repository. #[must_use] pub fn new() -> Self { Self { labels: Arc::new(Mutex::new(HashMap::new())), } } async fn labels(&self) -> MutexGuard<'_, HashMap> { self.labels.lock().await } } impl Default for MockRelayLabelRepository { fn default() -> Self { Self::new() } } #[async_trait] impl RelayLabelRepository for MockRelayLabelRepository { async fn get_label(&self, id: RelayId) -> Result, RepositoryError> { Ok(self.labels().await.get(&id.as_u8()).cloned()) } async fn save_label(&self, id: RelayId, label: RelayLabel) -> Result<(), RepositoryError> { self.labels().await.insert(id.as_u8(), label); Ok(()) } async fn get_all_labels(&self) -> Result, RepositoryError> { let mut result: Vec<(RelayId, RelayLabel)> = self .labels() .await .iter() .filter_map(|(&id, label)| { RelayId::new(id).map_or(None, |relay_id| Some((relay_id, label.clone()))) }) .collect(); // Sort by relay ID for consistent ordering result.sort_by_key(|(id, _)| id.as_u8()); Ok(result) } } #[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_get_label_returns_none_for_non_existent_relay() { // Test: get_label(RelayId(1)) → None when no label is set let repo = MockRelayLabelRepository::new(); let relay_id = RelayId::new(1).unwrap(); let result = repo.get_label(relay_id).await; assert!(result.is_ok()); assert!(result.unwrap().is_none()); } #[tokio::test] async fn test_save_label_and_get_label_returns_saved_label() { // Test: save_label(RelayId(1), "Pump") then get_label(RelayId(1)) → Some("Pump") let repo = MockRelayLabelRepository::new(); let relay_id = RelayId::new(1).unwrap(); let label = RelayLabel::new("Pump".to_string()).unwrap(); // Save the label let save_result = repo.save_label(relay_id, label.clone()).await; assert!(save_result.is_ok()); // Retrieve the label let get_result = repo.get_label(relay_id).await; assert!(get_result.is_ok()); let retrieved_label = get_result.unwrap(); assert!(retrieved_label.is_some()); assert_eq!(retrieved_label.unwrap().as_str(), "Pump"); } #[tokio::test] async fn test_save_label_overwrites_existing_label() { // Test: save_label twice with different values, get_label returns the latest let repo = MockRelayLabelRepository::new(); let relay_id = RelayId::new(1).unwrap(); let label1 = RelayLabel::new("First".to_string()).unwrap(); let label2 = RelayLabel::new("Second".to_string()).unwrap(); // Save first label repo.save_label(relay_id, label1).await.unwrap(); // Overwrite with second label repo.save_label(relay_id, label2).await.unwrap(); // Retrieve should return the second label let retrieved_label = repo.get_label(relay_id).await.unwrap(); assert!(retrieved_label.is_some()); assert_eq!(retrieved_label.unwrap().as_str(), "Second"); } #[tokio::test] async fn test_get_all_labels_returns_empty_vec_when_no_labels() { // Test: get_all_labels() → [] when repository is empty let repo = MockRelayLabelRepository::new(); let result = repo.get_all_labels().await; assert!(result.is_ok()); assert!(result.unwrap().is_empty()); } #[tokio::test] async fn test_get_all_labels_returns_all_saved_labels() { // Test: save multiple labels, get_all_labels() returns all of them let repo = MockRelayLabelRepository::new(); let relay1 = RelayId::new(1).unwrap(); let relay3 = RelayId::new(3).unwrap(); let relay5 = RelayId::new(5).unwrap(); let label1 = RelayLabel::new("Pump".to_string()).unwrap(); let label3 = RelayLabel::new("Heater".to_string()).unwrap(); let label5 = RelayLabel::new("Fan".to_string()).unwrap(); // Save labels repo.save_label(relay1, label1.clone()).await.unwrap(); repo.save_label(relay3, label3.clone()).await.unwrap(); repo.save_label(relay5, label5.clone()).await.unwrap(); // Retrieve all labels let result = repo.get_all_labels().await.unwrap(); assert_eq!(result.len(), 3); // Verify labels are sorted by relay ID assert_eq!(result[0].0.as_u8(), 1); assert_eq!(result[0].1.as_str(), "Pump"); assert_eq!(result[1].0.as_u8(), 3); assert_eq!(result[1].1.as_str(), "Heater"); assert_eq!(result[2].0.as_u8(), 5); assert_eq!(result[2].1.as_str(), "Fan"); } #[tokio::test] async fn test_get_all_labels_excludes_relays_without_labels() { // Test: Only relays with labels are returned, not all possible relay IDs let repo = MockRelayLabelRepository::new(); let relay2 = RelayId::new(2).unwrap(); let label2 = RelayLabel::new("Only This One".to_string()).unwrap(); repo.save_label(relay2, label2).await.unwrap(); let result = repo.get_all_labels().await.unwrap(); assert_eq!(result.len(), 1); assert_eq!(result[0].0.as_u8(), 2); } #[tokio::test] async fn test_save_label_for_all_valid_relay_ids() { // Test: All relay IDs (1-8) can have labels saved let repo = MockRelayLabelRepository::new(); for id in 1..=8 { let relay_id = RelayId::new(id).unwrap(); let label = RelayLabel::new(format!("Relay {id}")).unwrap(); let result = repo.save_label(relay_id, label).await; assert!(result.is_ok(), "Failed to save label for relay {id}"); } // Verify all labels were saved let all_labels = repo.get_all_labels().await.unwrap(); assert_eq!(all_labels.len(), 8); } #[tokio::test] async fn test_concurrent_access_is_thread_safe() { // Test: Multiple concurrent operations don't cause data races let repo = MockRelayLabelRepository::new(); let handles: Vec<_> = (1..=8) .map(|id| { let repo = repo.clone(); tokio::spawn(async move { let relay_id = RelayId::new(id).unwrap(); let label = RelayLabel::new(format!("Relay {id}")).unwrap(); repo.save_label(relay_id, label).await.unwrap(); }) }) .collect(); // Wait for all tasks to complete for handle in handles { handle.await.unwrap(); } // Verify all labels were saved correctly let all_labels = repo.get_all_labels().await.unwrap(); assert_eq!(all_labels.len(), 8); } }