diff --git a/src/handler/control.rs b/src/handler/control.rs index 686abcc..46c9f6f 100644 --- a/src/handler/control.rs +++ b/src/handler/control.rs @@ -18,6 +18,23 @@ use crate::{ AppState, }; +fn validate_unit_timing_order( + run_time_sec: i32, + acc_time_sec: i32, +) -> Result<(), ApiErr> { + if acc_time_sec <= run_time_sec { + return Err(ApiErr::BadRequest( + "acc_time_sec must be greater than run_time_sec".to_string(), + Some(json!({ + "run_time_sec": ["must be less than acc_time_sec"], + "acc_time_sec": ["must be greater than run_time_sec"] + })), + )); + } + + Ok(()) +} + #[derive(Debug, Deserialize, Validate)] pub struct GetUnitListQuery { #[validate(length(min = 1, max = 100))] @@ -293,13 +310,13 @@ pub struct CreateUnitReq { pub name: String, pub description: Option, pub enabled: Option, - #[validate(range(min = 0))] + #[validate(range(min = 1, message = "must be greater than 0"))] pub run_time_sec: Option, - #[validate(range(min = 0))] + #[validate(range(min = 1, message = "must be greater than 0"))] pub stop_time_sec: Option, - #[validate(range(min = 0))] + #[validate(range(min = 1, message = "must be greater than 0"))] pub acc_time_sec: Option, - #[validate(range(min = 0))] + #[validate(range(min = 1, message = "must be greater than 0"))] pub bl_time_sec: Option, pub require_manual_ack_after_fault: Option, } @@ -310,6 +327,33 @@ pub async fn create_unit( ) -> Result { payload.validate()?; + let run_time_sec = payload.run_time_sec.ok_or_else(|| { + ApiErr::BadRequest( + "run_time_sec is required".to_string(), + Some(json!({ "run_time_sec": ["is required"] })), + ) + })?; + let stop_time_sec = payload.stop_time_sec.ok_or_else(|| { + ApiErr::BadRequest( + "stop_time_sec is required".to_string(), + Some(json!({ "stop_time_sec": ["is required"] })), + ) + })?; + let acc_time_sec = payload.acc_time_sec.ok_or_else(|| { + ApiErr::BadRequest( + "acc_time_sec is required".to_string(), + Some(json!({ "acc_time_sec": ["is required"] })), + ) + })?; + let bl_time_sec = payload.bl_time_sec.ok_or_else(|| { + ApiErr::BadRequest( + "bl_time_sec is required".to_string(), + Some(json!({ "bl_time_sec": ["is required"] })), + ) + })?; + + validate_unit_timing_order(run_time_sec, acc_time_sec)?; + if crate::service::get_unit_by_code(&state.pool, &payload.code) .await? .is_some() @@ -327,10 +371,10 @@ pub async fn create_unit( name: &payload.name, description: payload.description.as_deref(), enabled: payload.enabled.unwrap_or(true), - run_time_sec: payload.run_time_sec.unwrap_or(0), - stop_time_sec: payload.stop_time_sec.unwrap_or(0), - acc_time_sec: payload.acc_time_sec.unwrap_or(0), - bl_time_sec: payload.bl_time_sec.unwrap_or(0), + run_time_sec, + stop_time_sec, + acc_time_sec, + bl_time_sec, require_manual_ack_after_fault: payload .require_manual_ack_after_fault .unwrap_or(true), @@ -355,13 +399,13 @@ pub struct UpdateUnitReq { pub name: Option, pub description: Option, pub enabled: Option, - #[validate(range(min = 0))] + #[validate(range(min = 1, message = "must be greater than 0"))] pub run_time_sec: Option, - #[validate(range(min = 0))] + #[validate(range(min = 1, message = "must be greater than 0"))] pub stop_time_sec: Option, - #[validate(range(min = 0))] + #[validate(range(min = 1, message = "must be greater than 0"))] pub acc_time_sec: Option, - #[validate(range(min = 0))] + #[validate(range(min = 1, message = "must be greater than 0"))] pub bl_time_sec: Option, pub require_manual_ack_after_fault: Option, } @@ -373,12 +417,14 @@ pub async fn update_unit( ) -> Result { payload.validate()?; - if crate::service::get_unit_by_id(&state.pool, unit_id) + let existing_unit = crate::service::get_unit_by_id(&state.pool, unit_id) .await? - .is_none() - { - return Err(ApiErr::NotFound("Unit not found".to_string(), None)); - } + .ok_or_else(|| ApiErr::NotFound("Unit not found".to_string(), None))?; + + validate_unit_timing_order( + payload.run_time_sec.unwrap_or(existing_unit.run_time_sec), + payload.acc_time_sec.unwrap_or(existing_unit.acc_time_sec), + )?; if let Some(code) = payload.code.as_deref() { let duplicate = crate::service::get_unit_by_code(&state.pool, code).await?; @@ -627,3 +673,52 @@ pub async fn get_unit_runtime( Ok(Json(runtime)) } +#[cfg(test)] +mod tests { + use super::{validate_unit_timing_order, CreateUnitReq, UpdateUnitReq}; + use validator::Validate; + + #[test] + fn create_unit_req_rejects_zero_second_fields() { + let payload = CreateUnitReq { + code: "U-01".to_string(), + name: "Unit 01".to_string(), + description: None, + enabled: Some(true), + run_time_sec: Some(0), + stop_time_sec: Some(10), + acc_time_sec: Some(20), + bl_time_sec: Some(5), + require_manual_ack_after_fault: Some(true), + }; + + assert!(payload.validate().is_err()); + } + + #[test] + fn create_unit_req_rejects_acc_time_not_greater_than_run_time() { + assert!(validate_unit_timing_order(10, 10).is_err()); + } + + #[test] + fn update_unit_req_rejects_zero_second_fields() { + let payload = UpdateUnitReq { + code: None, + name: None, + description: None, + enabled: None, + run_time_sec: None, + stop_time_sec: Some(0), + acc_time_sec: Some(20), + bl_time_sec: Some(5), + require_manual_ack_after_fault: None, + }; + + assert!(payload.validate().is_err()); + } + + #[test] + fn update_unit_req_rejects_acc_time_not_greater_than_run_time_when_both_present() { + assert!(validate_unit_timing_order(20, 15).is_err()); + } +} diff --git a/web/js/units.js b/web/js/units.js index 38bf6f9..6c87bd0 100644 --- a/web/js/units.js +++ b/web/js/units.js @@ -34,10 +34,10 @@ export function resetUnitForm() { dom.unitId.value = ""; dom.unitEnabled.checked = true; dom.unitManualAck.checked = true; - dom.unitRunTimeSec.value = "0"; - dom.unitStopTimeSec.value = "0"; - dom.unitAccTimeSec.value = "0"; - dom.unitBlTimeSec.value = "0"; + dom.unitRunTimeSec.value = "10"; + dom.unitStopTimeSec.value = "10"; + dom.unitAccTimeSec.value = "20"; + dom.unitBlTimeSec.value = "10"; } function openUnitModal() {