use async_trait::async_trait;
use cloacina::task_scheduler::{TriggerCondition, TriggerRule, ValueOperator};
use cloacina::*;
use std::sync::Arc;
#[derive(Clone)]
struct MockTask {
id: String,
deps: Vec<TaskNamespace>,
}
#[async_trait]
impl Task for MockTask {
async fn execute(
&self,
context: Context<serde_json::Value>,
) -> Result<Context<serde_json::Value>, TaskError> {
Ok(context)
}
fn id(&self) -> &str {
&self.id
}
fn dependencies(&self) -> &[TaskNamespace] {
&self.deps
}
}
#[test]
fn test_empty_workflow_returns_error() {
let workflow = Workflow::new("empty-wf");
let result = workflow.validate();
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
ValidationError::EmptyWorkflow
));
}
#[test]
fn test_duplicate_task_returns_error() {
let task1 = MockTask {
id: "task1".to_string(),
deps: vec![],
};
let task2 = MockTask {
id: "task1".to_string(), deps: vec![],
};
let result = Workflow::builder("dup-test")
.add_task(Arc::new(task1))
.expect("first add should succeed")
.add_task(Arc::new(task2));
let err = result.err().expect("Expected error for duplicate task");
match err {
WorkflowError::DuplicateTask(name) => {
assert!(name.contains("task1"));
}
other => panic!("Expected DuplicateTask, got: {:?}", other),
}
}
#[test]
fn test_missing_dependency_returns_error() {
let nonexistent_dep = TaskNamespace::new("public", "embedded", "missing-dep-wf", "ghost");
let task = MockTask {
id: "task1".to_string(),
deps: vec![nonexistent_dep],
};
let result = Workflow::builder("missing-dep-wf")
.add_task(Arc::new(task))
.expect("add should succeed")
.build();
let err = result.expect_err("Expected MissingDependency error from build()");
match err {
ValidationError::MissingDependency { task, dependency } => {
assert!(task.contains("task1"));
assert!(dependency.contains("ghost"));
}
other => panic!("Expected MissingDependency, got: {:?}", other),
}
}
#[test]
fn test_cyclic_dependency_returns_error() {
let wf_name = "cycle-wf";
let task2_ns = TaskNamespace::new("public", "embedded", wf_name, "task2");
let task1_ns = TaskNamespace::new("public", "embedded", wf_name, "task1");
let task1 = MockTask {
id: "task1".to_string(),
deps: vec![task2_ns],
};
let task2 = MockTask {
id: "task2".to_string(),
deps: vec![task1_ns],
};
let result = Workflow::builder(wf_name)
.add_task(Arc::new(task1))
.unwrap()
.add_task(Arc::new(task2))
.unwrap()
.build();
let err = result.expect_err("Expected CyclicDependency error from build()");
assert!(matches!(err, ValidationError::CyclicDependency { .. }));
}
#[test]
fn test_invalid_trigger_rule_json() {
let result = serde_json::from_str::<TriggerRule>("not json at all");
assert!(result.is_err());
}
#[test]
fn test_unknown_trigger_rule_type() {
let result = serde_json::from_str::<TriggerRule>(r#"{"type":"UnknownRule"}"#);
assert!(result.is_err());
}
#[test]
fn test_trigger_rule_all_missing_conditions() {
let result = serde_json::from_str::<TriggerRule>(r#"{"type":"All"}"#);
assert!(result.is_err());
}
#[test]
fn test_trigger_rule_conditions_wrong_type() {
let result =
serde_json::from_str::<TriggerRule>(r#"{"type":"All","conditions":"not_an_array"}"#);
assert!(result.is_err());
}
#[test]
fn test_unknown_condition_type() {
let result =
serde_json::from_str::<TriggerCondition>(r#"{"type":"UnknownCondition","task_name":"x"}"#);
assert!(result.is_err());
}
#[test]
fn test_context_value_condition_missing_fields() {
let result = serde_json::from_str::<TriggerCondition>(r#"{"type":"ContextValue","key":"k"}"#);
assert!(result.is_err());
}
#[test]
fn test_unknown_value_operator() {
let result = serde_json::from_str::<ValueOperator>(r#""UnknownOp""#);
assert!(result.is_err());
}
#[test]
fn test_context_duplicate_insert_returns_error() {
let mut ctx = Context::<serde_json::Value>::new();
ctx.insert("key1", serde_json::json!("val1")).unwrap();
let result = ctx.insert("key1", serde_json::json!("val2"));
assert!(result.is_err());
}
#[test]
fn test_context_update_missing_key_returns_error() {
let mut ctx = Context::<serde_json::Value>::new();
let result = ctx.update("nonexistent", serde_json::json!("val"));
assert!(result.is_err());
}
#[test]
fn test_context_get_missing_key_returns_none() {
let ctx = Context::<serde_json::Value>::new();
assert!(ctx.get("missing").is_none());
}
#[test]
fn test_cron_invalid_expression_error() {
use cloacina::cron_evaluator::CronEvaluator;
let result = CronEvaluator::new("invalid cron", "UTC");
assert!(result.is_err());
}
#[test]
fn test_cron_invalid_timezone_error() {
use cloacina::cron_evaluator::CronEvaluator;
let result = CronEvaluator::new("0 9 * * *", "Not/A/Timezone");
assert!(result.is_err());
}
#[test]
fn test_cron_empty_expression_error() {
use cloacina::cron_evaluator::CronEvaluator;
let result = CronEvaluator::new("", "UTC");
assert!(result.is_err());
}
#[test]
fn test_manifest_parse_duration_invalid() {
use cloacina::packaging::manifest_schema::parse_duration_str;
assert!(parse_duration_str("invalid").is_err());
assert!(parse_duration_str("5x").is_err());
assert!(parse_duration_str("").is_err());
assert!(parse_duration_str("abc123").is_err());
}
#[test]
fn test_manifest_parse_duration_valid() {
use cloacina::packaging::manifest_schema::parse_duration_str;
assert_eq!(
parse_duration_str("100ms").unwrap(),
std::time::Duration::from_millis(100)
);
assert_eq!(
parse_duration_str("5s").unwrap(),
std::time::Duration::from_secs(5)
);
assert_eq!(
parse_duration_str("2m").unwrap(),
std::time::Duration::from_secs(120)
);
assert_eq!(
parse_duration_str("1h").unwrap(),
std::time::Duration::from_secs(3600)
);
}