forge_core/testing/context/
cron.rs1use std::collections::HashMap;
4use std::sync::{Arc, RwLock};
5
6use chrono::{DateTime, Duration, Utc};
7use sqlx::PgPool;
8use uuid::Uuid;
9
10use super::super::mock_http::{MockHttp, MockRequest, MockResponse};
11use crate::env::{EnvAccess, EnvProvider, MockEnvProvider};
12use crate::function::AuthContext;
13
14#[derive(Debug, Clone)]
16pub struct TestLogEntry {
17 pub level: String,
19 pub message: String,
21 pub data: serde_json::Value,
23}
24
25#[derive(Clone)]
27pub struct TestCronLog {
28 cron_name: String,
29 entries: Arc<RwLock<Vec<TestLogEntry>>>,
30}
31
32impl TestCronLog {
33 pub fn new(cron_name: impl Into<String>) -> Self {
35 Self {
36 cron_name: cron_name.into(),
37 entries: Arc::new(RwLock::new(Vec::new())),
38 }
39 }
40
41 pub fn info(&self, message: &str) {
43 self.log("info", message, serde_json::Value::Null);
44 }
45
46 pub fn info_with(&self, message: &str, data: serde_json::Value) {
48 self.log("info", message, data);
49 }
50
51 pub fn warn(&self, message: &str) {
53 self.log("warn", message, serde_json::Value::Null);
54 }
55
56 pub fn warn_with(&self, message: &str, data: serde_json::Value) {
58 self.log("warn", message, data);
59 }
60
61 pub fn error(&self, message: &str) {
63 self.log("error", message, serde_json::Value::Null);
64 }
65
66 pub fn error_with(&self, message: &str, data: serde_json::Value) {
68 self.log("error", message, data);
69 }
70
71 pub fn debug(&self, message: &str) {
73 self.log("debug", message, serde_json::Value::Null);
74 }
75
76 fn log(&self, level: &str, message: &str, data: serde_json::Value) {
77 let entry = TestLogEntry {
78 level: level.to_string(),
79 message: message.to_string(),
80 data,
81 };
82 self.entries.write().unwrap().push(entry);
83 }
84
85 pub fn entries(&self) -> Vec<TestLogEntry> {
87 self.entries.read().unwrap().clone()
88 }
89
90 pub fn cron_name(&self) -> &str {
92 &self.cron_name
93 }
94}
95
96pub struct TestCronContext {
114 pub run_id: Uuid,
116 pub cron_name: String,
118 pub scheduled_time: DateTime<Utc>,
120 pub execution_time: DateTime<Utc>,
122 pub timezone: String,
124 pub is_catch_up: bool,
126 pub auth: AuthContext,
128 pub log: TestCronLog,
130 pool: Option<PgPool>,
132 http: Arc<MockHttp>,
134 env_provider: Arc<MockEnvProvider>,
136}
137
138impl TestCronContext {
139 pub fn builder(cron_name: impl Into<String>) -> TestCronContextBuilder {
141 TestCronContextBuilder::new(cron_name)
142 }
143
144 pub fn db(&self) -> Option<&PgPool> {
146 self.pool.as_ref()
147 }
148
149 pub fn http(&self) -> &MockHttp {
151 &self.http
152 }
153
154 pub fn delay(&self) -> Duration {
156 self.execution_time - self.scheduled_time
157 }
158
159 pub fn is_late(&self) -> bool {
161 self.delay() > Duration::minutes(1)
162 }
163
164 pub fn env_mock(&self) -> &MockEnvProvider {
166 &self.env_provider
167 }
168}
169
170impl EnvAccess for TestCronContext {
171 fn env_provider(&self) -> &dyn EnvProvider {
172 self.env_provider.as_ref()
173 }
174}
175
176pub struct TestCronContextBuilder {
178 run_id: Option<Uuid>,
179 cron_name: String,
180 scheduled_time: DateTime<Utc>,
181 execution_time: DateTime<Utc>,
182 timezone: String,
183 is_catch_up: bool,
184 user_id: Option<Uuid>,
185 roles: Vec<String>,
186 claims: HashMap<String, serde_json::Value>,
187 pool: Option<PgPool>,
188 http: MockHttp,
189 env_vars: HashMap<String, String>,
190}
191
192impl TestCronContextBuilder {
193 pub fn new(cron_name: impl Into<String>) -> Self {
195 let now = Utc::now();
196 Self {
197 run_id: None,
198 cron_name: cron_name.into(),
199 scheduled_time: now,
200 execution_time: now,
201 timezone: "UTC".to_string(),
202 is_catch_up: false,
203 user_id: None,
204 roles: Vec::new(),
205 claims: HashMap::new(),
206 pool: None,
207 http: MockHttp::new(),
208 env_vars: HashMap::new(),
209 }
210 }
211
212 pub fn with_run_id(mut self, id: Uuid) -> Self {
214 self.run_id = Some(id);
215 self
216 }
217
218 pub fn scheduled_at(mut self, time: DateTime<Utc>) -> Self {
220 self.scheduled_time = time;
221 self
222 }
223
224 pub fn executed_at(mut self, time: DateTime<Utc>) -> Self {
226 self.execution_time = time;
227 self
228 }
229
230 pub fn with_timezone(mut self, tz: impl Into<String>) -> Self {
232 self.timezone = tz.into();
233 self
234 }
235
236 pub fn as_catch_up(mut self) -> Self {
238 self.is_catch_up = true;
239 self
240 }
241
242 pub fn as_user(mut self, id: Uuid) -> Self {
244 self.user_id = Some(id);
245 self
246 }
247
248 pub fn with_role(mut self, role: impl Into<String>) -> Self {
250 self.roles.push(role.into());
251 self
252 }
253
254 pub fn with_pool(mut self, pool: PgPool) -> Self {
256 self.pool = Some(pool);
257 self
258 }
259
260 pub fn mock_http<F>(self, pattern: &str, handler: F) -> Self
262 where
263 F: Fn(&MockRequest) -> MockResponse + Send + Sync + 'static,
264 {
265 self.http.add_mock_sync(pattern, handler);
266 self
267 }
268
269 pub fn mock_http_json<T: serde::Serialize>(self, pattern: &str, response: T) -> Self {
271 let json = serde_json::to_value(response).unwrap_or(serde_json::Value::Null);
272 self.mock_http(pattern, move |_| MockResponse::json(json.clone()))
273 }
274
275 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
277 self.env_vars.insert(key.into(), value.into());
278 self
279 }
280
281 pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
283 self.env_vars.extend(vars);
284 self
285 }
286
287 pub fn build(self) -> TestCronContext {
289 let auth = if let Some(user_id) = self.user_id {
290 AuthContext::authenticated(user_id, self.roles, self.claims)
291 } else {
292 AuthContext::unauthenticated()
293 };
294
295 TestCronContext {
296 run_id: self.run_id.unwrap_or_else(Uuid::new_v4),
297 cron_name: self.cron_name.clone(),
298 scheduled_time: self.scheduled_time,
299 execution_time: self.execution_time,
300 timezone: self.timezone,
301 is_catch_up: self.is_catch_up,
302 auth,
303 log: TestCronLog::new(self.cron_name),
304 pool: self.pool,
305 http: Arc::new(self.http),
306 env_provider: Arc::new(MockEnvProvider::with_vars(self.env_vars)),
307 }
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn test_cron_context_creation() {
317 let ctx = TestCronContext::builder("daily_cleanup").build();
318
319 assert_eq!(ctx.cron_name, "daily_cleanup");
320 assert!(!ctx.is_catch_up);
321 assert!(!ctx.is_late());
322 }
323
324 #[test]
325 fn test_catch_up_run() {
326 let ctx = TestCronContext::builder("hourly_sync")
327 .as_catch_up()
328 .build();
329
330 assert!(ctx.is_catch_up);
331 }
332
333 #[test]
334 fn test_late_detection() {
335 let scheduled = Utc::now() - Duration::minutes(5);
336 let ctx = TestCronContext::builder("quick_task")
337 .scheduled_at(scheduled)
338 .build();
339
340 assert!(ctx.is_late());
341 assert!(ctx.delay() >= Duration::minutes(4));
342 }
343
344 #[test]
345 fn test_logging() {
346 let ctx = TestCronContext::builder("test_cron").build();
347
348 ctx.log.info("Starting");
349 ctx.log.warn("Warning message");
350 ctx.log.error("Error occurred");
351
352 let entries = ctx.log.entries();
353 assert_eq!(entries.len(), 3);
354 assert_eq!(entries[0].level, "info");
355 assert_eq!(entries[1].level, "warn");
356 assert_eq!(entries[2].level, "error");
357 }
358
359 #[test]
360 fn test_timezone() {
361 let ctx = TestCronContext::builder("tz_test")
362 .with_timezone("America/New_York")
363 .build();
364
365 assert_eq!(ctx.timezone, "America/New_York");
366 }
367}