Create in-memory mock implementation of RelayLabelRepository trait using HashMap with Arc<Mutex<>> for thread-safe concurrent access. Includes 8 comprehensive tests covering all trait methods and edge cases. Also refactor domain repository module structure to support multiple repository types (repository.rs → repository/label.rs + mod.rs). TDD phase: Combined red-green (tests + implementation) Ref: T037, T038
238 lines
7.9 KiB
Rust
238 lines
7.9 KiB
Rust
//! 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<Mutex<_>>` 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<Mutex<HashMap<u8, RelayLabel>>>,
|
|
}
|
|
|
|
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<u8, RelayLabel>> {
|
|
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<Option<RelayLabel>, 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<Vec<(RelayId, RelayLabel)>, 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);
|
|
}
|
|
}
|