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,
21 pub message: String,
22 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 {
34 Self {
35 cron_name: cron_name.into(),
36 entries: Arc::new(RwLock::new(Vec::new())),
37 }
38 }
39
40 pub fn info(&self, message: &str) {
41 self.log("info", message, serde_json::Value::Null);
42 }
43
44 pub fn info_with(&self, message: &str, data: serde_json::Value) {
45 self.log("info", message, data);
46 }
47
48 pub fn warn(&self, message: &str) {
49 self.log("warn", message, serde_json::Value::Null);
50 }
51
52 pub fn warn_with(&self, message: &str, data: serde_json::Value) {
53 self.log("warn", message, data);
54 }
55
56 pub fn error(&self, message: &str) {
57 self.log("error", message, serde_json::Value::Null);
58 }
59
60 pub fn error_with(&self, message: &str, data: serde_json::Value) {
61 self.log("error", message, data);
62 }
63
64 pub fn debug(&self, message: &str) {
65 self.log("debug", message, serde_json::Value::Null);
66 }
67
68 fn log(&self, level: &str, message: &str, data: serde_json::Value) {
69 let entry = TestLogEntry {
70 level: level.to_string(),
71 message: message.to_string(),
72 data,
73 };
74 self.entries.write().unwrap().push(entry);
75 }
76
77 pub fn entries(&self) -> Vec<TestLogEntry> {
78 self.entries.read().unwrap().clone()
79 }
80
81 pub fn cron_name(&self) -> &str {
82 &self.cron_name
83 }
84}
85
86pub struct TestCronContext {
88 pub run_id: Uuid,
89 pub cron_name: String,
90 pub scheduled_time: DateTime<Utc>,
91 pub execution_time: DateTime<Utc>,
92 pub timezone: String,
93 pub is_catch_up: bool,
94 pub auth: AuthContext,
95 pub log: TestCronLog,
96 pool: Option<PgPool>,
97 http: Arc<MockHttp>,
98 env_provider: Arc<MockEnvProvider>,
99}
100
101impl TestCronContext {
102 pub fn builder(cron_name: impl Into<String>) -> TestCronContextBuilder {
103 TestCronContextBuilder::new(cron_name)
104 }
105
106 pub fn db(&self) -> Option<&PgPool> {
107 self.pool.as_ref()
108 }
109
110 pub fn http(&self) -> &MockHttp {
111 &self.http
112 }
113
114 pub fn delay(&self) -> Duration {
115 self.execution_time - self.scheduled_time
116 }
117
118 pub fn is_late(&self) -> bool {
120 self.delay() > Duration::minutes(1)
121 }
122
123 pub fn env_mock(&self) -> &MockEnvProvider {
124 &self.env_provider
125 }
126}
127
128impl EnvAccess for TestCronContext {
129 fn env_provider(&self) -> &dyn EnvProvider {
130 self.env_provider.as_ref()
131 }
132}
133
134pub struct TestCronContextBuilder {
135 run_id: Option<Uuid>,
136 cron_name: String,
137 scheduled_time: DateTime<Utc>,
138 execution_time: DateTime<Utc>,
139 timezone: String,
140 is_catch_up: bool,
141 user_id: Option<Uuid>,
142 roles: Vec<String>,
143 claims: HashMap<String, serde_json::Value>,
144 pool: Option<PgPool>,
145 http: MockHttp,
146 env_vars: HashMap<String, String>,
147}
148
149impl TestCronContextBuilder {
150 pub fn new(cron_name: impl Into<String>) -> Self {
151 let now = Utc::now();
152 Self {
153 run_id: None,
154 cron_name: cron_name.into(),
155 scheduled_time: now,
156 execution_time: now,
157 timezone: "UTC".to_string(),
158 is_catch_up: false,
159 user_id: None,
160 roles: Vec::new(),
161 claims: HashMap::new(),
162 pool: None,
163 http: MockHttp::new(),
164 env_vars: HashMap::new(),
165 }
166 }
167
168 pub fn with_run_id(mut self, id: Uuid) -> Self {
169 self.run_id = Some(id);
170 self
171 }
172
173 pub fn scheduled_at(mut self, time: DateTime<Utc>) -> Self {
174 self.scheduled_time = time;
175 self
176 }
177
178 pub fn executed_at(mut self, time: DateTime<Utc>) -> Self {
179 self.execution_time = time;
180 self
181 }
182
183 pub fn with_timezone(mut self, tz: impl Into<String>) -> Self {
184 self.timezone = tz.into();
185 self
186 }
187
188 pub fn as_catch_up(mut self) -> Self {
189 self.is_catch_up = true;
190 self
191 }
192
193 pub fn as_user(mut self, id: Uuid) -> Self {
194 self.user_id = Some(id);
195 self
196 }
197
198 pub fn as_subject(mut self, subject: impl Into<String>) -> Self {
200 self.claims
201 .insert("sub".to_string(), serde_json::json!(subject.into()));
202 self
203 }
204
205 pub fn with_role(mut self, role: impl Into<String>) -> Self {
206 self.roles.push(role.into());
207 self
208 }
209
210 pub fn with_roles(mut self, roles: Vec<String>) -> Self {
211 self.roles.extend(roles);
212 self
213 }
214
215 pub fn with_claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
216 self.claims.insert(key.into(), value);
217 self
218 }
219
220 pub fn with_pool(mut self, pool: PgPool) -> Self {
221 self.pool = Some(pool);
222 self
223 }
224
225 pub fn mock_http<F>(self, pattern: &str, handler: F) -> Self
226 where
227 F: Fn(&MockRequest) -> MockResponse + Send + Sync + 'static,
228 {
229 self.http.add_mock_sync(pattern, handler);
230 self
231 }
232
233 pub fn mock_http_json<T: serde::Serialize>(self, pattern: &str, response: T) -> Self {
234 let json = serde_json::to_value(response).unwrap_or(serde_json::Value::Null);
235 self.mock_http(pattern, move |_| MockResponse::json(json.clone()))
236 }
237
238 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
239 self.env_vars.insert(key.into(), value.into());
240 self
241 }
242
243 pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
244 self.env_vars.extend(vars);
245 self
246 }
247
248 pub fn build(self) -> TestCronContext {
249 TestCronContext {
250 run_id: self.run_id.unwrap_or_else(Uuid::new_v4),
251 cron_name: self.cron_name.clone(),
252 scheduled_time: self.scheduled_time,
253 execution_time: self.execution_time,
254 timezone: self.timezone,
255 is_catch_up: self.is_catch_up,
256 auth: build_test_auth(self.user_id, self.roles, self.claims),
257 log: TestCronLog::new(self.cron_name),
258 pool: self.pool,
259 http: Arc::new(self.http),
260 env_provider: Arc::new(MockEnvProvider::with_vars(self.env_vars)),
261 }
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_cron_context_creation() {
271 let ctx = TestCronContext::builder("daily_cleanup").build();
272
273 assert_eq!(ctx.cron_name, "daily_cleanup");
274 assert!(!ctx.is_catch_up);
275 assert!(!ctx.is_late());
276 }
277
278 #[test]
279 fn test_catch_up_run() {
280 let ctx = TestCronContext::builder("hourly_sync")
281 .as_catch_up()
282 .build();
283
284 assert!(ctx.is_catch_up);
285 }
286
287 #[test]
288 fn test_late_detection() {
289 let scheduled = Utc::now() - Duration::minutes(5);
290 let ctx = TestCronContext::builder("quick_task")
291 .scheduled_at(scheduled)
292 .build();
293
294 assert!(ctx.is_late());
295 assert!(ctx.delay() >= Duration::minutes(4));
296 }
297
298 #[test]
299 fn test_logging() {
300 let ctx = TestCronContext::builder("test_cron").build();
301
302 ctx.log.info("Starting");
303 ctx.log.warn("Warning message");
304 ctx.log.error("Error occurred");
305
306 let entries = ctx.log.entries();
307 assert_eq!(entries.len(), 3);
308 assert_eq!(entries[0].level, "info");
309 assert_eq!(entries[1].level, "warn");
310 assert_eq!(entries[2].level, "error");
311 }
312
313 #[test]
314 fn test_timezone() {
315 let ctx = TestCronContext::builder("tz_test")
316 .with_timezone("America/New_York")
317 .build();
318
319 assert_eq!(ctx.timezone, "America/New_York");
320 }
321}