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