test: cover automation trigger API behavior

This commit is contained in:
Bryan Helmkamp 2026-05-28 12:56:14 -04:00
parent 91d11eb04d
commit 0106711170
No known key found for this signature in database
2 changed files with 140 additions and 2 deletions

View file

@ -11,7 +11,7 @@
pub mod auth;
#[allow(
dead_code,
reason = "Automation scheduler wiring will call the materializer; issue #398 adds the shared core first."
reason = "Automation materializer test hooks and helpers are only referenced by selected targets."
)]
mod automation_materializer;
mod canonical_host;

View file

@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use axum::body::Body;
use axum::http::{Method, Request, StatusCode, header};
@ -180,6 +180,56 @@ fn revision_from(body: &Value) -> &str {
.expect("automation response should include a revision")
}
fn assert_schedule_trigger(body: &Value, expression: &str, enabled: bool) {
let trigger = body["triggers"]
.as_array()
.expect("automation response should include triggers")
.iter()
.find(|trigger| trigger["id"] == "nightly")
.expect("automation response should include nightly schedule trigger");
assert_eq!(
trigger,
&json!({
"type": "schedule",
"id": "nightly",
"enabled": enabled,
"expression": expression
})
);
}
async fn persisted_automation_toml(automation_dir: &Path, id: &str) -> toml::Value {
let persisted = tokio::fs::read_to_string(automation_dir.join(format!("{id}.toml")))
.await
.expect("persisted automation TOML should be readable");
toml::from_str(&persisted).expect("persisted automation TOML should parse")
}
fn assert_persisted_schedule_trigger(body: &toml::Value, expression: &str, enabled: bool) {
let triggers = body
.get("triggers")
.and_then(toml::Value::as_array)
.expect("persisted automation TOML should include triggers");
let trigger = triggers
.iter()
.find(|trigger| trigger.get("id").and_then(toml::Value::as_str) == Some("nightly"))
.expect("persisted automation TOML should include nightly trigger");
assert_eq!(
trigger.get("type").and_then(toml::Value::as_str),
Some("schedule")
);
assert_eq!(
trigger.get("expression").and_then(toml::Value::as_str),
Some(expression)
);
assert_eq!(
trigger.get("enabled").and_then(toml::Value::as_bool),
Some(enabled)
);
}
#[tokio::test]
async fn empty_automation_list_returns_total_zero() {
let (app, _temp_dir, _automation_dir) = automation_app();
@ -212,6 +262,35 @@ async fn create_automation_persists_sibling_toml_file() {
assert!(automation_dir.join("nightly.toml").exists());
}
#[tokio::test]
async fn schedule_trigger_round_trips_through_create_list_get_and_toml() {
let (app, _temp_dir, automation_dir) = automation_app();
let created = create_automation(&app, "nightly", "Nightly").await;
assert_schedule_trigger(&created, "0 3 * * *", true);
let response = app
.clone()
.oneshot(empty_request(Method::GET, "/automations/nightly"))
.await
.expect("get automation should respond");
let retrieved =
response_json(response, StatusCode::OK, "GET /api/v1/automations/nightly").await;
assert_schedule_trigger(&retrieved, "0 3 * * *", true);
let response = app
.oneshot(empty_request(Method::GET, "/automations"))
.await
.expect("list automations should respond");
let list = response_json(response, StatusCode::OK, "GET /api/v1/automations").await;
assert_schedule_trigger(&list["data"][0], "0 3 * * *", true);
let persisted = persisted_automation_toml(&automation_dir, "nightly").await;
assert_persisted_schedule_trigger(&persisted, "0 3 * * *", true);
assert!(persisted.get("id").is_none());
assert!(persisted.get("revision").is_none());
}
#[tokio::test]
async fn list_automations_returns_items_sorted_by_id() {
let (app, _temp_dir, _automation_dir) = automation_app();
@ -306,6 +385,42 @@ async fn replace_automation_accepts_unquoted_if_match_and_returns_new_etag() {
assert_eq!(etag, format!("\"{}\"", revision_from(&body)));
}
#[tokio::test]
async fn replace_automation_round_trips_schedule_trigger() {
let (app, _temp_dir, automation_dir) = automation_app();
let created = create_automation(&app, "nightly", "Nightly").await;
let revision = revision_from(&created);
let mut replacement = replacement_body("Rescheduled");
replacement["triggers"] = json!([
{
"type": "api",
"id": "manual",
"enabled": true
},
{
"type": "schedule",
"id": "nightly",
"enabled": false,
"expression": "30 4 * * *"
}
]);
let response = app
.oneshot(request_with_if_match(
Method::PUT,
"/automations/nightly",
revision,
Some(replacement),
))
.await
.expect("replace automation should respond");
let body = response_json(response, StatusCode::OK, "PUT /api/v1/automations/nightly").await;
assert_schedule_trigger(&body, "30 4 * * *", false);
let persisted = persisted_automation_toml(&automation_dir, "nightly").await;
assert_persisted_schedule_trigger(&persisted, "30 4 * * *", false);
}
#[tokio::test]
async fn stale_automation_replace_returns_conflict() {
let (app, _temp_dir, _automation_dir) = automation_app();
@ -416,6 +531,29 @@ async fn delete_automation_removes_file_and_resource() {
.await;
}
#[tokio::test]
async fn unknown_trigger_type_is_unprocessable() {
let (app, _temp_dir, _automation_dir) = automation_app();
let mut body = automation_body("nightly", "Nightly");
body["triggers"][1] = json!({
"type": "event",
"id": "event_trigger",
"enabled": true
});
let response = app
.oneshot(json_request(Method::POST, "/automations", &body))
.await
.expect("unknown trigger type create should respond");
response_status(
response,
StatusCode::UNPROCESSABLE_ENTITY,
"POST /api/v1/automations unknown trigger type",
)
.await;
}
#[tokio::test]
async fn invalid_trigger_ids_are_unprocessable() {
let (app, _temp_dir, _automation_dir) = automation_app();