//! Comprehensive tests for `RelayLabelRepository` trait contract. //! //! This module provides a reusable test suite that verifies any implementation //! of the `RelayLabelRepository` trait meets the expected contract. These tests //! can be run against different implementations (mock, SQLite, PostgreSQL, etc.) //! to ensure they all behave correctly. //! //! **T035**: Write tests for RelayLabelRepository trait //! - Test: `get_label(RelayId(1)) → Option` //! - Test: `save_label(RelayId(1), label) → Result<(), RepositoryError>` //! - Test: `delete_label(RelayId(1)) → Result<(), RepositoryError>` #[cfg(test)] mod relay_label_repository_contract_tests { use crate::domain::relay::{ repository::RelayLabelRepository, types::{RelayId, RelayLabel}, }; // ========================================================================= // get_label() Tests // ========================================================================= /// Test: `get_label` returns None for non-existent relay /// /// Verifies that querying a relay ID that has no label returns None /// rather than an error. pub async fn test_get_label_returns_none_for_non_existent_relay( repo: &R, ) { let relay_id = RelayId::new(1).expect("Valid relay ID"); let result = repo.get_label(relay_id).await; assert!(result.is_ok(), "get_label should succeed"); assert!( result.unwrap().is_none(), "get_label should return None for non-existent relay" ); } /// Test: `get_label` retrieves previously saved label /// /// Verifies that after saving a label, `get_label` returns the same label. pub async fn test_get_label_retrieves_saved_label(repo: &R) { let relay_id = RelayId::new(2).expect("Valid relay ID"); let label = RelayLabel::new("Heater".to_string()).expect("Valid label"); // Save the label repo.save_label(relay_id, label.clone()) .await .expect("save_label should succeed"); // Retrieve the label let result = repo.get_label(relay_id).await; assert!(result.is_ok(), "get_label should succeed"); let retrieved = result.unwrap(); assert!(retrieved.is_some(), "get_label should return Some"); assert_eq!( retrieved.unwrap().as_str(), "Heater", "Retrieved label should match saved label" ); } /// Test: `get_label` returns None after label is deleted /// /// Verifies that after deleting a label, `get_label` returns None. pub async fn test_get_label_returns_none_after_delete(repo: &R) { let relay_id = RelayId::new(3).expect("Valid relay ID"); let label = RelayLabel::new("ToBeDeleted".to_string()).expect("Valid label"); // Save and then delete the label repo.save_label(relay_id, label) .await .expect("save_label should succeed"); repo.delete_label(relay_id) .await .expect("delete_label should succeed"); // Verify it's gone let result = repo.get_label(relay_id).await; assert!(result.is_ok(), "get_label should succeed"); assert!( result.unwrap().is_none(), "get_label should return None after delete" ); } // ========================================================================= // save_label() Tests // ========================================================================= /// Test: `save_label` successfully saves a label /// /// Verifies that `save_label` returns Ok and stores the label. pub async fn test_save_label_succeeds(repo: &R) { let relay_id = RelayId::new(1).expect("Valid relay ID"); let label = RelayLabel::new("Pump".to_string()).expect("Valid label"); let result = repo.save_label(relay_id, label).await; assert!(result.is_ok(), "save_label should succeed"); } /// Test: `save_label` overwrites existing label /// /// Verifies that calling `save_label` multiple times for the same relay ID /// replaces the old label with the new one. pub async fn test_save_label_overwrites_existing_label(repo: &R) { let relay_id = RelayId::new(4).expect("Valid relay ID"); let label1 = RelayLabel::new("First".to_string()).expect("Valid label"); let label2 = RelayLabel::new("Second".to_string()).expect("Valid label"); // Save first label repo.save_label(relay_id, label1) .await .expect("First save should succeed"); // Overwrite with second label repo.save_label(relay_id, label2) .await .expect("Second save should succeed"); // Verify only the second label is present let result = repo .get_label(relay_id) .await .expect("get_label should succeed"); assert!(result.is_some(), "Label should exist"); assert_eq!( result.unwrap().as_str(), "Second", "Label should be updated to second value" ); } /// Test: `save_label` works for all valid relay IDs (1-8) /// /// Verifies that all relay IDs in the valid range can have labels saved. pub async fn test_save_label_for_all_valid_relay_ids(repo: &R) { for id in 1..=8 { let relay_id = RelayId::new(id).expect("Valid relay ID"); let label = RelayLabel::new(format!("Relay {id}")).expect("Valid label"); let result = repo.save_label(relay_id, label).await; assert!( result.is_ok(), "save_label should succeed for relay ID {id}" ); } // Verify all labels were saved let all_labels = repo .get_all_labels() .await .expect("get_all_labels should succeed"); assert_eq!(all_labels.len(), 8, "Should have all 8 relay labels"); } /// Test: `save_label` accepts maximum length labels /// /// Verifies that labels at the maximum allowed length (50 characters) /// can be saved successfully. pub async fn test_save_label_accepts_max_length_labels(repo: &R) { let relay_id = RelayId::new(5).expect("Valid relay ID"); let max_label = RelayLabel::new("A".repeat(50)).expect("Valid max-length label"); let result = repo.save_label(relay_id, max_label).await; assert!( result.is_ok(), "save_label should succeed with max-length label" ); // Verify it was saved correctly let retrieved = repo .get_label(relay_id) .await .expect("get_label should succeed"); assert!(retrieved.is_some(), "Label should be saved"); assert_eq!( retrieved.unwrap().as_str().len(), 50, "Label should have correct length" ); } /// Test: `save_label` accepts minimum length labels /// /// Verifies that labels at the minimum allowed length (1 character) /// can be saved successfully. pub async fn test_save_label_accepts_min_length_labels(repo: &R) { let relay_id = RelayId::new(6).expect("Valid relay ID"); let min_label = RelayLabel::new("X".to_string()).expect("Valid min-length label"); let result = repo.save_label(relay_id, min_label).await; assert!( result.is_ok(), "save_label should succeed with min-length label" ); // Verify it was saved correctly let retrieved = repo .get_label(relay_id) .await .expect("get_label should succeed"); assert!(retrieved.is_some(), "Label should be saved"); assert_eq!(retrieved.unwrap().as_str(), "X", "Label should match"); } // ========================================================================= // delete_label() Tests // ========================================================================= /// Test: `delete_label` succeeds for existing label /// /// Verifies that `delete_label` returns Ok when deleting an existing label. pub async fn test_delete_label_succeeds_for_existing_label(repo: &R) { let relay_id = RelayId::new(7).expect("Valid relay ID"); let label = RelayLabel::new("ToDelete".to_string()).expect("Valid label"); // Save the label first repo.save_label(relay_id, label) .await .expect("save_label should succeed"); // Delete it let result = repo.delete_label(relay_id).await; assert!(result.is_ok(), "delete_label should succeed"); } /// Test: `delete_label` succeeds for non-existent label /// /// Verifies that `delete_label` returns Ok even when no label exists /// (idempotent operation). pub async fn test_delete_label_succeeds_for_non_existent_label( repo: &R, ) { let relay_id = RelayId::new(8).expect("Valid relay ID"); // Delete without saving first let result = repo.delete_label(relay_id).await; assert!( result.is_ok(), "delete_label should succeed even if label doesn't exist" ); } /// Test: `delete_label` removes label from repository /// /// Verifies that after deleting a label, it no longer appears in `get_label` /// or `get_all_labels` results. pub async fn test_delete_label_removes_label_from_repository( repo: &R, ) { let relay1 = RelayId::new(1).expect("Valid relay ID"); let relay2 = RelayId::new(2).expect("Valid relay ID"); let label1 = RelayLabel::new("Keep".to_string()).expect("Valid label"); let label2 = RelayLabel::new("Remove".to_string()).expect("Valid label"); // Save two labels repo.save_label(relay1, label1) .await .expect("save should succeed"); repo.save_label(relay2, label2) .await .expect("save should succeed"); // Delete one label repo.delete_label(relay2) .await .expect("delete should succeed"); // Verify deleted label is gone let get_result = repo .get_label(relay2) .await .expect("get_label should succeed"); assert!(get_result.is_none(), "Deleted label should not exist"); // Verify other label still exists let other_result = repo .get_label(relay1) .await .expect("get_label should succeed"); assert!(other_result.is_some(), "Other label should still exist"); // Verify get_all_labels only returns the remaining label let all_labels = repo .get_all_labels() .await .expect("get_all_labels should succeed"); assert_eq!(all_labels.len(), 1, "Should only have one label remaining"); assert_eq!(all_labels[0].0.as_u8(), 1, "Should be relay 1"); } /// Test: `delete_label` is idempotent /// /// Verifies that calling `delete_label` multiple times succeeds without error. pub async fn test_delete_label_is_idempotent(repo: &R) { let relay_id = RelayId::new(3).expect("Valid relay ID"); let label = RelayLabel::new("Idempotent".to_string()).expect("Valid label"); // Save, then delete twice repo.save_label(relay_id, label) .await .expect("save should succeed"); repo.delete_label(relay_id) .await .expect("First delete should succeed"); let second_delete = repo.delete_label(relay_id).await; assert!( second_delete.is_ok(), "Second delete should succeed (idempotent)" ); } // ========================================================================= // get_all_labels() Tests // ========================================================================= /// Test: `get_all_labels` returns empty vector when no labels exist /// /// Verifies that `get_all_labels` returns an empty vector rather than /// an error when the repository is empty. pub async fn test_get_all_labels_returns_empty_when_no_labels( repo: &R, ) { let result = repo.get_all_labels().await; assert!(result.is_ok(), "get_all_labels should succeed"); assert!( result.unwrap().is_empty(), "get_all_labels should return empty vector" ); } /// Test: `get_all_labels` returns all saved labels /// /// Verifies that `get_all_labels` returns all labels that have been saved, /// and only those relays with labels. pub async fn test_get_all_labels_returns_all_saved_labels(repo: &R) { let relay1 = RelayId::new(1).expect("Valid relay ID"); let relay3 = RelayId::new(3).expect("Valid relay ID"); let relay5 = RelayId::new(5).expect("Valid relay ID"); let label1 = RelayLabel::new("Pump".to_string()).expect("Valid label"); let label3 = RelayLabel::new("Heater".to_string()).expect("Valid label"); let label5 = RelayLabel::new("Fan".to_string()).expect("Valid label"); // Save labels repo.save_label(relay1, label1.clone()) .await .expect("Save should succeed"); repo.save_label(relay3, label3.clone()) .await .expect("Save should succeed"); repo.save_label(relay5, label5.clone()) .await .expect("Save should succeed"); // Retrieve all labels let result = repo .get_all_labels() .await .expect("get_all_labels should succeed"); assert_eq!(result.len(), 3, "Should return exactly 3 labels"); // Verify the labels are present (order may vary by implementation) let has_relay1 = result .iter() .any(|(id, label)| id.as_u8() == 1 && label.as_str() == "Pump"); let has_relay3 = result .iter() .any(|(id, label)| id.as_u8() == 3 && label.as_str() == "Heater"); let has_relay5 = result .iter() .any(|(id, label)| id.as_u8() == 5 && label.as_str() == "Fan"); assert!(has_relay1, "Should contain relay 1 with label 'Pump'"); assert!(has_relay3, "Should contain relay 3 with label 'Heater'"); assert!(has_relay5, "Should contain relay 5 with label 'Fan'"); } /// Test: `get_all_labels` excludes relays without labels /// /// Verifies that only relays with labels are returned, not all possible /// relay IDs (1-8). pub async fn test_get_all_labels_excludes_relays_without_labels( repo: &R, ) { let relay2 = RelayId::new(2).expect("Valid relay ID"); let label2 = RelayLabel::new("Only This One".to_string()).expect("Valid label"); repo.save_label(relay2, label2) .await .expect("Save should succeed"); let result = repo .get_all_labels() .await .expect("get_all_labels should succeed"); assert_eq!( result.len(), 1, "Should return only the one relay with a label" ); assert_eq!(result[0].0.as_u8(), 2, "Should be relay 2"); } /// Test: `get_all_labels` excludes deleted labels /// /// Verifies that deleted labels don't appear in `get_all_labels` results. pub async fn test_get_all_labels_excludes_deleted_labels(repo: &R) { let relay1 = RelayId::new(1).expect("Valid relay ID"); let relay2 = RelayId::new(2).expect("Valid relay ID"); let relay3 = RelayId::new(3).expect("Valid relay ID"); let label1 = RelayLabel::new("Keep1".to_string()).expect("Valid label"); let label2 = RelayLabel::new("Delete".to_string()).expect("Valid label"); let label3 = RelayLabel::new("Keep2".to_string()).expect("Valid label"); // Save all three labels repo.save_label(relay1, label1) .await .expect("save should succeed"); repo.save_label(relay2, label2) .await .expect("save should succeed"); repo.save_label(relay3, label3) .await .expect("save should succeed"); // Delete the middle one repo.delete_label(relay2) .await .expect("delete should succeed"); // Verify get_all_labels only returns the two remaining labels let result = repo .get_all_labels() .await .expect("get_all_labels should succeed"); assert_eq!(result.len(), 2, "Should have 2 labels after deletion"); let has_relay1 = result.iter().any(|(id, _)| id.as_u8() == 1); let has_relay2 = result.iter().any(|(id, _)| id.as_u8() == 2); let has_relay3 = result.iter().any(|(id, _)| id.as_u8() == 3); assert!(has_relay1, "Relay 1 should be present"); assert!(!has_relay2, "Relay 2 should NOT be present (deleted)"); assert!(has_relay3, "Relay 3 should be present"); } }