1use serde::{Deserialize, Serialize};
2use time::OffsetDateTime;
3use uuid::Uuid;
4
5use crate::validation::{Validate, ValidationError};
6
7pub const TASK_TYPE_FETCH_RELEASES: &str = "fetch_releases";
18
19pub const TASK_TYPE_DETECT_VERSION: &str = "detect_version";
25
26#[derive(Serialize, Deserialize)]
28#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
29pub struct ScheduledTaskResponse {
30 pub id: Uuid,
31 pub task_type: String,
32 pub label: String,
33 pub interval_seconds: i32,
34 pub jitter_seconds: i32,
35 pub enabled: bool,
36 pub task_config: Option<serde_json::Value>,
37 #[serde(with = "time::serde::rfc3339::option")]
38 #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = DateTime))]
39 pub last_run_at: Option<OffsetDateTime>,
40 #[serde(with = "time::serde::rfc3339")]
41 #[cfg_attr(feature = "openapi", schema(value_type = String, format = DateTime))]
42 pub next_run_at: OffsetDateTime,
43 pub is_running: bool,
44 pub last_error: Option<String>,
45 pub run_count: i64,
46 #[serde(with = "time::serde::rfc3339")]
47 #[cfg_attr(feature = "openapi", schema(value_type = String, format = DateTime))]
48 pub created_at: OffsetDateTime,
49 #[serde(with = "time::serde::rfc3339")]
50 #[cfg_attr(feature = "openapi", schema(value_type = String, format = DateTime))]
51 pub updated_at: OffsetDateTime,
52}
53
54#[derive(Serialize, Deserialize)]
56#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
57pub struct UpdateScheduledTaskRequest {
58 pub interval_seconds: Option<i32>,
60 pub jitter_seconds: Option<i32>,
62 pub enabled: Option<bool>,
64 pub task_config: Option<serde_json::Value>,
66}
67
68#[derive(Serialize, Deserialize)]
70#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
71pub struct TriggerScheduledTaskResponse {
72 pub triggered: bool,
74 pub message: String,
76}
77
78impl Validate for UpdateScheduledTaskRequest {
79 fn validate(&self) -> Result<(), ValidationError> {
80 if let Some(interval) = self.interval_seconds
81 && interval <= 0
82 {
83 return Err(ValidationError {
84 field: "interval_seconds",
85 message: "must be greater than 0".to_string(),
86 });
87 }
88
89 if let Some(jitter) = self.jitter_seconds
90 && jitter < 0
91 {
92 return Err(ValidationError {
93 field: "jitter_seconds",
94 message: "must be >= 0".to_string(),
95 });
96 }
97
98 Ok(())
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 fn sample_uuid() -> Uuid {
107 Uuid::parse_str("a1a2a3a4-b1b2-c1c2-d1d2-e1e2e3e4e5e6")
108 .expect("hard-coded UUID should be valid")
109 }
110
111 #[test]
114 fn scheduled_task_response_round_trip_all_fields() {
115 use time::macros::datetime;
116 let resp = ScheduledTaskResponse {
117 id: sample_uuid(),
118 task_type: "fetch_releases".to_string(),
119 label: "Fetch Latest Releases".to_string(),
120 interval_seconds: 21600,
121 jitter_seconds: 300,
122 enabled: true,
123 task_config: Some(serde_json::json!({"timeout": 30})),
124 last_run_at: Some(datetime!(2025-06-01 00:00:00 UTC)),
125 next_run_at: datetime!(2025-06-02 00:00:00 UTC),
126 is_running: false,
127 last_error: Some("timeout exceeded".to_string()),
128 run_count: 42,
129 created_at: datetime!(2025-01-01 00:00:00 UTC),
130 updated_at: datetime!(2025-06-01 00:00:00 UTC),
131 };
132 let json = serde_json::to_string(&resp).expect("serialization should succeed");
133 let deserialized: ScheduledTaskResponse =
134 serde_json::from_str(&json).expect("deserialization should succeed");
135 assert_eq!(deserialized.id, sample_uuid());
136 assert_eq!(deserialized.task_type, "fetch_releases");
137 assert_eq!(deserialized.label, "Fetch Latest Releases");
138 assert_eq!(deserialized.interval_seconds, 21600);
139 assert_eq!(deserialized.jitter_seconds, 300);
140 assert!(deserialized.enabled);
141 assert!(deserialized.task_config.is_some());
142 assert_eq!(
143 deserialized.last_run_at,
144 Some(datetime!(2025-06-01 00:00:00 UTC))
145 );
146 assert_eq!(deserialized.next_run_at, datetime!(2025-06-02 00:00:00 UTC));
147 assert!(!deserialized.is_running);
148 assert_eq!(deserialized.last_error.as_deref(), Some("timeout exceeded"));
149 assert_eq!(deserialized.run_count, 42);
150 }
151
152 #[test]
153 fn scheduled_task_response_round_trip_none_fields() {
154 use time::macros::datetime;
155 let resp = ScheduledTaskResponse {
156 id: sample_uuid(),
157 task_type: "cleanup".to_string(),
158 label: "Weekly cleanup".to_string(),
159 interval_seconds: 86400,
160 jitter_seconds: 300,
161 enabled: false,
162 task_config: None,
163 last_run_at: None,
164 next_run_at: datetime!(2025-06-08 00:00:00 UTC),
165 is_running: false,
166 last_error: None,
167 run_count: 0,
168 created_at: datetime!(2025-01-01 00:00:00 UTC),
169 updated_at: datetime!(2025-01-01 00:00:00 UTC),
170 };
171 let json = serde_json::to_string(&resp).expect("serialization should succeed");
172 let deserialized: ScheduledTaskResponse =
173 serde_json::from_str(&json).expect("deserialization should succeed");
174 assert!(deserialized.task_config.is_none());
175 assert!(deserialized.last_run_at.is_none());
176 assert!(deserialized.last_error.is_none());
177 assert!(!deserialized.enabled);
178 assert_eq!(deserialized.run_count, 0);
179 }
180
181 #[test]
182 fn scheduled_task_response_is_running_true() {
183 use time::macros::datetime;
184 let resp = ScheduledTaskResponse {
185 id: sample_uuid(),
186 task_type: "sync".to_string(),
187 label: "Sync".to_string(),
188 interval_seconds: 300,
189 jitter_seconds: 30,
190 enabled: true,
191 task_config: None,
192 last_run_at: None,
193 next_run_at: datetime!(2025-06-01 00:05:00 UTC),
194 is_running: true,
195 last_error: None,
196 run_count: 10,
197 created_at: datetime!(2025-01-01 00:00:00 UTC),
198 updated_at: datetime!(2025-01-01 00:00:00 UTC),
199 };
200 let json = serde_json::to_string(&resp).expect("serialization should succeed");
201 let deserialized: ScheduledTaskResponse =
202 serde_json::from_str(&json).expect("deserialization should succeed");
203 assert!(deserialized.is_running);
204 }
205
206 #[test]
209 fn update_scheduled_task_request_round_trip_all_fields() {
210 let req = UpdateScheduledTaskRequest {
211 interval_seconds: Some(7200),
212 jitter_seconds: Some(60),
213 enabled: Some(false),
214 task_config: Some(serde_json::json!({"retries": 3})),
215 };
216 let json = serde_json::to_string(&req).expect("serialization should succeed");
217 let deserialized: UpdateScheduledTaskRequest =
218 serde_json::from_str(&json).expect("deserialization should succeed");
219 assert_eq!(deserialized.interval_seconds, Some(7200));
220 assert_eq!(deserialized.jitter_seconds, Some(60));
221 assert_eq!(deserialized.enabled, Some(false));
222 assert!(deserialized.task_config.is_some());
223 }
224
225 #[test]
226 fn update_scheduled_task_request_round_trip_none_fields() {
227 let req = UpdateScheduledTaskRequest {
228 interval_seconds: None,
229 jitter_seconds: None,
230 enabled: None,
231 task_config: None,
232 };
233 let json = serde_json::to_string(&req).expect("serialization should succeed");
234 let deserialized: UpdateScheduledTaskRequest =
235 serde_json::from_str(&json).expect("deserialization should succeed");
236 assert!(deserialized.interval_seconds.is_none());
237 assert!(deserialized.jitter_seconds.is_none());
238 assert!(deserialized.enabled.is_none());
239 assert!(deserialized.task_config.is_none());
240 }
241
242 #[test]
243 fn update_scheduled_task_request_from_empty_json() {
244 let json = r#"{}"#;
245 let req: UpdateScheduledTaskRequest =
246 serde_json::from_str(json).expect("deserialization should succeed");
247 assert!(req.interval_seconds.is_none());
248 assert!(req.jitter_seconds.is_none());
249 assert!(req.enabled.is_none());
250 assert!(req.task_config.is_none());
251 }
252
253 #[test]
256 fn validate_valid_interval() {
257 let req = UpdateScheduledTaskRequest {
258 interval_seconds: Some(300),
259 jitter_seconds: Some(30),
260 enabled: None,
261 task_config: None,
262 };
263 assert!(req.validate().is_ok());
264 }
265
266 #[test]
267 fn validate_none_interval_passes() {
268 let req = UpdateScheduledTaskRequest {
269 interval_seconds: None,
270 jitter_seconds: None,
271 enabled: Some(true),
272 task_config: None,
273 };
274 assert!(req.validate().is_ok());
275 }
276
277 #[test]
278 fn validate_zero_interval_fails() {
279 let req = UpdateScheduledTaskRequest {
280 interval_seconds: Some(0),
281 jitter_seconds: None,
282 enabled: None,
283 task_config: None,
284 };
285 let err = req
286 .validate()
287 .expect_err("zero interval should fail validation");
288 assert_eq!(err.field, "interval_seconds");
289 assert!(err.message.contains("greater than 0"));
290 }
291
292 #[test]
293 fn validate_negative_interval_fails() {
294 let req = UpdateScheduledTaskRequest {
295 interval_seconds: Some(-1),
296 jitter_seconds: None,
297 enabled: None,
298 task_config: None,
299 };
300 let err = req
301 .validate()
302 .expect_err("negative interval should fail validation");
303 assert_eq!(err.field, "interval_seconds");
304 }
305
306 #[test]
307 fn validate_negative_jitter_fails() {
308 let req = UpdateScheduledTaskRequest {
309 interval_seconds: None,
310 jitter_seconds: Some(-1),
311 enabled: None,
312 task_config: None,
313 };
314 let err = req
315 .validate()
316 .expect_err("negative jitter should fail validation");
317 assert_eq!(err.field, "jitter_seconds");
318 assert!(err.message.contains(">= 0"));
319 }
320
321 #[test]
322 fn validate_zero_jitter_passes() {
323 let req = UpdateScheduledTaskRequest {
324 interval_seconds: Some(300),
325 jitter_seconds: Some(0),
326 enabled: None,
327 task_config: None,
328 };
329 assert!(req.validate().is_ok());
330 }
331
332 #[test]
335 fn trigger_scheduled_task_response_round_trip() {
336 let resp = TriggerScheduledTaskResponse {
337 triggered: true,
338 message: "Task started".to_string(),
339 };
340 let json = serde_json::to_string(&resp).expect("serialization should succeed");
341 let deserialized: TriggerScheduledTaskResponse =
342 serde_json::from_str(&json).expect("deserialization should succeed");
343 assert!(deserialized.triggered);
344 assert_eq!(deserialized.message, "Task started");
345 }
346
347 #[test]
348 fn trigger_scheduled_task_response_not_triggered() {
349 let resp = TriggerScheduledTaskResponse {
350 triggered: false,
351 message: "Task is already running".to_string(),
352 };
353 let json = serde_json::to_string(&resp).expect("serialization should succeed");
354 let deserialized: TriggerScheduledTaskResponse =
355 serde_json::from_str(&json).expect("deserialization should succeed");
356 assert!(!deserialized.triggered);
357 assert_eq!(deserialized.message, "Task is already running");
358 }
359}