forge_core/testing/context/
cron.rs1#![allow(clippy::unwrap_used, clippy::indexing_slicing)]
4
5use std::collections::HashMap;
6use std::sync::{Arc, RwLock};
7
8use chrono::{DateTime, Duration, Utc};
9use sqlx::PgPool;
10use uuid::Uuid;
11
12use super::super::mock_http::{MockHttp, MockRequest, MockResponse};
13use super::build_test_auth;
14use crate::env::{EnvAccess, EnvProvider, MockEnvProvider};
15use crate::function::AuthContext;
16
17#[derive(Debug, Clone)]
19pub struct TestLogEntry {
20 pub level: String,
22 pub message: String,
24 pub data: serde_json::Value,
26}
27
28#[derive(Clone)]
30pub struct TestCronLog {
31 cron_name: String,
32 entries: Arc<RwLock<Vec<TestLogEntry>>>,
33}
34
35impl TestCronLog {
36 pub fn new(cron_name: impl Into<String>) -> Self {
38 Self {
39 cron_name: cron_name.into(),
40 entries: Arc::new(RwLock::new(Vec::new())),
41 }
42 }
43
44 pub fn info(&self, message: &str) {
46 self.log("info", message, serde_json::Value::Null);
47 }
48
49 pub fn info_with(&self, message: &str, data: serde_json::Value) {
51 self.log("info", message, data);
52 }
53
54 pub fn warn(&self, message: &str) {
56 self.log("warn", message, serde_json::Value::Null);
57 }
58
59 pub fn warn_with(&self, message: &str, data: serde_json::Value) {
61 self.log("warn", message, data);
62 }
63
64 pub fn error(&self, message: &str) {
66 self.log("error", message, serde_json::Value::Null);
67 }
68
69 pub fn error_with(&self, message: &str, data: serde_json::Value) {
71 self.log("error", message, data);
72 }
73
74 pub fn debug(&self, message: &str) {
76 self.log("debug", message, serde_json::Value::Null);
77 }
78
79 fn log(&self, level: &str, message: &str, data: serde_json::Value) {
80 let entry = TestLogEntry {
81 level: level.to_string(),
82 message: message.to_string(),
83 data,
84 };
85 self.entries.write().unwrap().push(entry);
86 }
87
88 pub fn entries(&self) -> Vec<TestLogEntry> {
90 self.entries.read().unwrap().clone()
91 }
92
93 pub fn cron_name(&self) -> &str {
95 &self.cron_name
96 }
97}
98
99pub struct TestCronContext {
117 pub run_id: Uuid,
119 pub cron_name: String,
121 pub scheduled_time: DateTime<Utc>,
123 pub execution_time: DateTime<Utc>,
125 pub timezone: String,
127 pub is_catch_up: bool,
129 pub auth: AuthContext,
131 pub log: TestCronLog,
133 pool: Option<PgPool>,
135 http: Arc<MockHttp>,
137 env_provider: Arc<MockEnvProvider>,
139}
140
141impl TestCronContext {
142 pub fn builder(cron_name: impl Into<String>) -> TestCronContextBuilder {
144 TestCronContextBuilder::new(cron_name)
145 }
146
147 pub fn db(&self) -> Option<&PgPool> {
149 self.pool.as_ref()
150 }
151
152 pub fn http(&self) -> &MockHttp {
154 &self.http
155 }
156
157 pub fn delay(&self) -> Duration {
159 self.execution_time - self.scheduled_time
160 }
161
162 pub fn is_late(&self) -> bool {
164 self.delay() > Duration::minutes(1)
165 }
166
167 pub fn env_mock(&self) -> &MockEnvProvider {
169 &self.env_provider
170 }
171}
172
173impl EnvAccess for TestCronContext {
174 fn env_provider(&self) -> &dyn EnvProvider {
175 self.env_provider.as_ref()
176 }
177}
178
179pub struct TestCronContextBuilder {
181 run_id: Option<Uuid>,
182 cron_name: String,
183 scheduled_time: DateTime<Utc>,
184 execution_time: DateTime<Utc>,
185 timezone: String,
186 is_catch_up: bool,
187 user_id: Option<Uuid>,
188 roles: Vec<String>,
189 claims: HashMap<String, serde_json::Value>,
190 pool: Option<PgPool>,
191 http: MockHttp,
192 env_vars: HashMap<String, String>,
193}
194
195impl TestCronContextBuilder {
196 pub fn new(cron_name: impl Into<String>) -> Self {
198 let now = Utc::now();
199 Self {
200 run_id: None,
201 cron_name: cron_name.into(),
202 scheduled_time: now,
203 execution_time: now,
204 timezone: "UTC".to_string(),
205 is_catch_up: false,
206 user_id: None,
207 roles: Vec::new(),
208 claims: HashMap::new(),
209 pool: None,
210 http: MockHttp::new(),
211 env_vars: HashMap::new(),
212 }
213 }
214
215 pub fn with_run_id(mut self, id: Uuid) -> Self {
217 self.run_id = Some(id);
218 self
219 }
220
221 pub fn scheduled_at(mut self, time: DateTime<Utc>) -> Self {
223 self.scheduled_time = time;
224 self
225 }
226
227 pub fn executed_at(mut self, time: DateTime<Utc>) -> Self {
229 self.execution_time = time;
230 self
231 }
232
233 pub fn with_timezone(mut self, tz: impl Into<String>) -> Self {
235 self.timezone = tz.into();
236 self
237 }
238
239 pub fn as_catch_up(mut self) -> Self {
241 self.is_catch_up = true;
242 self
243 }
244
245 pub fn as_user(mut self, id: Uuid) -> Self {
247 self.user_id = Some(id);
248 self
249 }
250
251 pub fn as_subject(mut self, subject: impl Into<String>) -> Self {
253 self.claims
254 .insert("sub".to_string(), serde_json::json!(subject.into()));
255 self
256 }
257
258 pub fn with_role(mut self, role: impl Into<String>) -> Self {
260 self.roles.push(role.into());
261 self
262 }
263
264 pub fn with_pool(mut self, pool: PgPool) -> Self {
266 self.pool = Some(pool);
267 self
268 }
269
270 pub fn mock_http<F>(self, pattern: &str, handler: F) -> Self
272 where
273 F: Fn(&MockRequest) -> MockResponse + Send + Sync + 'static,
274 {
275 self.http.add_mock_sync(pattern, handler);
276 self
277 }
278
279 pub fn mock_http_json<T: serde::Serialize>(self, pattern: &str, response: T) -> Self {
281 let json = serde_json::to_value(response).unwrap_or(serde_json::Value::Null);
282 self.mock_http(pattern, move |_| MockResponse::json(json.clone()))
283 }
284
285 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
287 self.env_vars.insert(key.into(), value.into());
288 self
289 }
290
291 pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
293 self.env_vars.extend(vars);
294 self
295 }
296
297 pub fn build(self) -> TestCronContext {
299 TestCronContext {
300 run_id: self.run_id.unwrap_or_else(Uuid::new_v4),
301 cron_name: self.cron_name.clone(),
302 scheduled_time: self.scheduled_time,
303 execution_time: self.execution_time,
304 timezone: self.timezone,
305 is_catch_up: self.is_catch_up,
306 auth: build_test_auth(self.user_id, self.roles, self.claims),
307 log: TestCronLog::new(self.cron_name),
308 pool: self.pool,
309 http: Arc::new(self.http),
310 env_provider: Arc::new(MockEnvProvider::with_vars(self.env_vars)),
311 }
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn test_cron_context_creation() {
321 let ctx = TestCronContext::builder("daily_cleanup").build();
322
323 assert_eq!(ctx.cron_name, "daily_cleanup");
324 assert!(!ctx.is_catch_up);
325 assert!(!ctx.is_late());
326 }
327
328 #[test]
329 fn test_catch_up_run() {
330 let ctx = TestCronContext::builder("hourly_sync")
331 .as_catch_up()
332 .build();
333
334 assert!(ctx.is_catch_up);
335 }
336
337 #[test]
338 fn test_late_detection() {
339 let scheduled = Utc::now() - Duration::minutes(5);
340 let ctx = TestCronContext::builder("quick_task")
341 .scheduled_at(scheduled)
342 .build();
343
344 assert!(ctx.is_late());
345 assert!(ctx.delay() >= Duration::minutes(4));
346 }
347
348 #[test]
349 fn test_logging() {
350 let ctx = TestCronContext::builder("test_cron").build();
351
352 ctx.log.info("Starting");
353 ctx.log.warn("Warning message");
354 ctx.log.error("Error occurred");
355
356 let entries = ctx.log.entries();
357 assert_eq!(entries.len(), 3);
358 assert_eq!(entries[0].level, "info");
359 assert_eq!(entries[1].level, "warn");
360 assert_eq!(entries[2].level, "error");
361 }
362
363 #[test]
364 fn test_timezone() {
365 let ctx = TestCronContext::builder("tz_test")
366 .with_timezone("America/New_York")
367 .build();
368
369 assert_eq!(ctx.timezone, "America/New_York");
370 }
371}