Files
sta/backend/src/infrastructure/persistence/label_repository.rs
Lucien Cartier-Tilet 306fa38935 test(infrastructure): implement MockRelayLabelRepository for testing
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
2026-01-22 00:57:11 +01:00

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);
}
}