🧪 test(rule_processor): add comprehensive unit tests for orchestration and state setters
This commit is contained in:
committed by
Jeremiah Russell
parent
992414e0b9
commit
1012047d89
@@ -432,3 +432,306 @@ impl RuleProcessor for GmailClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{rules::EolRule, EolAction, Error};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
/// Test helper to create a simple EolRule with or without a query
|
||||||
|
fn create_test_rule(id: usize, has_query: bool) -> EolRule {
|
||||||
|
use crate::{MessageAge, Retention};
|
||||||
|
|
||||||
|
let mut rule = EolRule::new(id);
|
||||||
|
|
||||||
|
if has_query {
|
||||||
|
// Create a rule that will generate a query (using retention days)
|
||||||
|
let retention = Retention::new(MessageAge::Days(30), false);
|
||||||
|
rule.set_retention(retention);
|
||||||
|
rule.add_label("test-label");
|
||||||
|
}
|
||||||
|
// For rules without query, we just return the basic rule with no retention set
|
||||||
|
|
||||||
|
rule
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fake client implementation for testing the orchestration logic
|
||||||
|
struct FakeClient {
|
||||||
|
labels: Vec<String>,
|
||||||
|
label_ids: Vec<String>,
|
||||||
|
query: String,
|
||||||
|
messages_prepared: bool,
|
||||||
|
prepare_call_count: u32,
|
||||||
|
batch_trash_call_count: Arc<Mutex<u32>>, // Use Arc<Mutex> for thread safety
|
||||||
|
should_fail_add_labels: bool,
|
||||||
|
should_fail_prepare: bool,
|
||||||
|
should_fail_batch_trash: bool,
|
||||||
|
simulate_missing_labels: bool, // Flag to simulate labels not being found
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FakeClient {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
labels: Vec::new(),
|
||||||
|
label_ids: Vec::new(),
|
||||||
|
query: String::new(),
|
||||||
|
messages_prepared: false,
|
||||||
|
prepare_call_count: 0,
|
||||||
|
batch_trash_call_count: Arc::new(Mutex::new(0)),
|
||||||
|
should_fail_add_labels: false,
|
||||||
|
should_fail_prepare: false,
|
||||||
|
should_fail_batch_trash: false,
|
||||||
|
simulate_missing_labels: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FakeClient {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a client that simulates missing labels (add_labels succeeds but no label_ids)
|
||||||
|
fn with_missing_labels() -> Self {
|
||||||
|
let mut client = Self::default();
|
||||||
|
client.simulate_missing_labels = true;
|
||||||
|
// This client will accept add_labels but won't populate label_ids,
|
||||||
|
// simulating the case where labels don't exist in the mailbox
|
||||||
|
client
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_labels(label_ids: Vec<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
label_ids,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_failure(failure_type: &str) -> Self {
|
||||||
|
let mut client = Self::default();
|
||||||
|
match failure_type {
|
||||||
|
"add_labels" => client.should_fail_add_labels = true,
|
||||||
|
"prepare" => client.should_fail_prepare = true,
|
||||||
|
"batch_trash" => client.should_fail_batch_trash = true,
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
|
client
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_batch_trash_call_count(&self) -> u32 {
|
||||||
|
*self.batch_trash_call_count.lock().unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MailOperations for FakeClient {
|
||||||
|
fn add_labels(&mut self, labels: &[String]) -> Result<()> {
|
||||||
|
if self.should_fail_add_labels {
|
||||||
|
return Err(Error::DirectoryUnset); // Use a valid error variant
|
||||||
|
}
|
||||||
|
self.labels.extend(labels.iter().cloned());
|
||||||
|
// Only populate label_ids if we're not simulating missing labels
|
||||||
|
if !self.simulate_missing_labels && !labels.is_empty() {
|
||||||
|
self.label_ids = labels.to_vec();
|
||||||
|
}
|
||||||
|
// When simulate_missing_labels is true, label_ids stays empty
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn label_ids(&self) -> Vec<String> {
|
||||||
|
self.label_ids.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_query(&mut self, query: &str) {
|
||||||
|
self.query = query.to_owned();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare(&mut self, _pages: u32) -> Result<()> {
|
||||||
|
// Always increment the counter to track that prepare was called
|
||||||
|
self.prepare_call_count += 1;
|
||||||
|
|
||||||
|
if self.should_fail_prepare {
|
||||||
|
return Err(Error::NoLabelsFound); // Use a valid error variant
|
||||||
|
}
|
||||||
|
self.messages_prepared = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn batch_trash(&self) -> Result<()> {
|
||||||
|
// Always increment the counter to track that batch_trash was called
|
||||||
|
*self.batch_trash_call_count.lock().unwrap() += 1;
|
||||||
|
|
||||||
|
if self.should_fail_batch_trash {
|
||||||
|
return Err(Error::InvalidPagingMode); // Use a valid error variant
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_errors_when_label_missing() {
|
||||||
|
let mut client = FakeClient::with_missing_labels(); // Simulate labels not being found
|
||||||
|
let rule = create_test_rule(1, true);
|
||||||
|
let label = "missing-label";
|
||||||
|
|
||||||
|
let result = process_label_with_rule(&mut client, &rule, label, 0, false).await;
|
||||||
|
|
||||||
|
assert!(matches!(result, Err(Error::LabelNotFoundInMailbox(_))));
|
||||||
|
assert_eq!(client.prepare_call_count, 0);
|
||||||
|
assert_eq!(client.get_batch_trash_call_count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_errors_when_rule_has_no_query() {
|
||||||
|
let mut client = FakeClient::with_labels(vec!["test-label".to_string()]);
|
||||||
|
let rule = create_test_rule(2, false); // Rule without query
|
||||||
|
let label = "test-label";
|
||||||
|
|
||||||
|
let result = process_label_with_rule(&mut client, &rule, label, 0, false).await;
|
||||||
|
|
||||||
|
assert!(matches!(result, Err(Error::NoQueryStringCalculated(2))));
|
||||||
|
assert_eq!(client.prepare_call_count, 0);
|
||||||
|
assert_eq!(client.get_batch_trash_call_count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_dry_run_does_not_trash() {
|
||||||
|
let mut client = FakeClient::with_labels(vec!["test-label".to_string()]);
|
||||||
|
let rule = create_test_rule(3, true);
|
||||||
|
let label = "test-label";
|
||||||
|
|
||||||
|
let result = process_label_with_rule(&mut client, &rule, label, 0, false).await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(client.prepare_call_count, 1);
|
||||||
|
assert_eq!(client.get_batch_trash_call_count(), 0); // Should not trash in dry-run mode
|
||||||
|
assert!(client.messages_prepared);
|
||||||
|
assert!(!client.query.is_empty()); // Query should be set
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_trashes_messages_once() {
|
||||||
|
let mut client = FakeClient::with_labels(vec!["test-label".to_string()]);
|
||||||
|
let rule = create_test_rule(4, true);
|
||||||
|
let label = "test-label";
|
||||||
|
|
||||||
|
let result = process_label_with_rule(&mut client, &rule, label, 0, true).await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(client.prepare_call_count, 1);
|
||||||
|
assert_eq!(client.get_batch_trash_call_count(), 1); // Should trash when execute=true
|
||||||
|
assert!(client.messages_prepared);
|
||||||
|
assert!(!client.query.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_propagates_prepare_error() {
|
||||||
|
// Create a client that will fail on prepare but has valid labels
|
||||||
|
let mut client = FakeClient::with_labels(vec!["test-label".to_string()]);
|
||||||
|
client.should_fail_prepare = true; // Set the failure flag directly
|
||||||
|
|
||||||
|
let rule = create_test_rule(5, true);
|
||||||
|
let label = "test-label";
|
||||||
|
|
||||||
|
let result = process_label_with_rule(&mut client, &rule, label, 0, true).await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(client.prepare_call_count, 1); // prepare should be called once
|
||||||
|
assert_eq!(client.get_batch_trash_call_count(), 0); // Should not reach trash due to error
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_propagates_batch_trash_error() {
|
||||||
|
// Create a client that will fail on batch_trash but has valid labels
|
||||||
|
let mut client = FakeClient::with_labels(vec!["test-label".to_string()]);
|
||||||
|
client.should_fail_batch_trash = true; // Set the failure flag directly
|
||||||
|
|
||||||
|
let rule = create_test_rule(6, true);
|
||||||
|
let label = "test-label";
|
||||||
|
|
||||||
|
let result = process_label_with_rule(&mut client, &rule, label, 0, true).await;
|
||||||
|
|
||||||
|
assert!(result.is_err());
|
||||||
|
assert_eq!(client.prepare_call_count, 1);
|
||||||
|
assert_eq!(client.get_batch_trash_call_count(), 1); // Should attempt trash but fail
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_pages_parameter_passed_correctly() {
|
||||||
|
let mut client = FakeClient::with_labels(vec!["test-label".to_string()]);
|
||||||
|
let rule = create_test_rule(7, true);
|
||||||
|
let label = "test-label";
|
||||||
|
let pages = 5;
|
||||||
|
|
||||||
|
let result = process_label_with_rule(&mut client, &rule, label, pages, false).await;
|
||||||
|
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert_eq!(client.prepare_call_count, 1);
|
||||||
|
// Note: In a more sophisticated test, we'd verify pages parameter is passed to prepare
|
||||||
|
// but our simple FakeClient doesn't track this. In practice, you might want to enhance it.
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test the rule processor trait setters and getters
|
||||||
|
#[test]
|
||||||
|
fn test_rule_processor_setters_and_getters() {
|
||||||
|
// Note: This test would need a mock GmailClient implementation
|
||||||
|
// For now, we'll create a simple struct that implements RuleProcessor
|
||||||
|
|
||||||
|
struct MockProcessor {
|
||||||
|
rule: Option<EolRule>,
|
||||||
|
execute: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RuleProcessor for MockProcessor {
|
||||||
|
fn set_rule(&mut self, rule: EolRule) {
|
||||||
|
self.rule = Some(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_execute(&mut self, value: bool) {
|
||||||
|
self.execute = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action(&self) -> Option<EolAction> {
|
||||||
|
self.rule.as_ref().and_then(|r| r.action())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn find_rule_and_messages_for_label(&mut self, _label: &str) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn prepare(&mut self, _pages: u32) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn batch_delete(&self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn batch_trash(&self) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut processor = MockProcessor {
|
||||||
|
rule: None,
|
||||||
|
execute: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test initial state
|
||||||
|
assert!(processor.action().is_none());
|
||||||
|
assert!(!processor.execute);
|
||||||
|
|
||||||
|
// Test rule setting
|
||||||
|
let rule = create_test_rule(8, true);
|
||||||
|
processor.set_rule(rule);
|
||||||
|
assert!(processor.action().is_some());
|
||||||
|
assert_eq!(processor.action(), Some(EolAction::Trash));
|
||||||
|
|
||||||
|
// Test execute flag setting
|
||||||
|
processor.set_execute(true);
|
||||||
|
assert!(processor.execute);
|
||||||
|
|
||||||
|
processor.set_execute(false);
|
||||||
|
assert!(!processor.execute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user