forge_core/testing/context/
job.rs1use std::collections::HashMap;
4use std::sync::{Arc, RwLock};
5
6use sqlx::PgPool;
7use uuid::Uuid;
8
9use super::super::mock_http::{MockHttp, MockRequest, MockResponse};
10use super::build_test_auth;
11use crate::Result;
12use crate::env::{EnvAccess, EnvProvider, MockEnvProvider};
13use crate::function::AuthContext;
14
15#[derive(Debug, Clone)]
17pub struct TestProgressUpdate {
18 pub percent: u8,
20 pub message: String,
22}
23
24pub struct TestJobContext {
43 pub job_id: Uuid,
45 pub job_type: String,
47 pub attempt: u32,
49 pub max_attempts: u32,
51 pub auth: AuthContext,
53 pool: Option<PgPool>,
55 http: Arc<MockHttp>,
57 progress_updates: Arc<RwLock<Vec<TestProgressUpdate>>>,
59 env_provider: Arc<MockEnvProvider>,
61}
62
63impl TestJobContext {
64 pub fn builder(job_type: impl Into<String>) -> TestJobContextBuilder {
66 TestJobContextBuilder::new(job_type)
67 }
68
69 pub fn db(&self) -> Option<&PgPool> {
71 self.pool.as_ref()
72 }
73
74 pub fn http(&self) -> &MockHttp {
76 &self.http
77 }
78
79 pub fn progress(&self, percent: u8, message: impl Into<String>) -> Result<()> {
81 let update = TestProgressUpdate {
82 percent: percent.min(100),
83 message: message.into(),
84 };
85 self.progress_updates.write().unwrap().push(update);
86 Ok(())
87 }
88
89 pub fn progress_updates(&self) -> Vec<TestProgressUpdate> {
91 self.progress_updates.read().unwrap().clone()
92 }
93
94 pub fn is_retry(&self) -> bool {
96 self.attempt > 1
97 }
98
99 pub fn is_last_attempt(&self) -> bool {
101 self.attempt >= self.max_attempts
102 }
103
104 pub async fn heartbeat(&self) -> Result<()> {
106 Ok(())
107 }
108
109 pub fn env_mock(&self) -> &MockEnvProvider {
111 &self.env_provider
112 }
113}
114
115impl EnvAccess for TestJobContext {
116 fn env_provider(&self) -> &dyn EnvProvider {
117 self.env_provider.as_ref()
118 }
119}
120
121pub struct TestJobContextBuilder {
123 job_id: Option<Uuid>,
124 job_type: String,
125 attempt: u32,
126 max_attempts: u32,
127 user_id: Option<Uuid>,
128 roles: Vec<String>,
129 claims: HashMap<String, serde_json::Value>,
130 pool: Option<PgPool>,
131 http: MockHttp,
132 env_vars: HashMap<String, String>,
133}
134
135impl TestJobContextBuilder {
136 pub fn new(job_type: impl Into<String>) -> Self {
138 Self {
139 job_id: None,
140 job_type: job_type.into(),
141 attempt: 1,
142 max_attempts: 1,
143 user_id: None,
144 roles: Vec::new(),
145 claims: HashMap::new(),
146 pool: None,
147 http: MockHttp::new(),
148 env_vars: HashMap::new(),
149 }
150 }
151
152 pub fn with_job_id(mut self, id: Uuid) -> Self {
154 self.job_id = Some(id);
155 self
156 }
157
158 pub fn as_retry(mut self, attempt: u32) -> Self {
160 self.attempt = attempt.max(1);
161 self
162 }
163
164 pub fn with_max_attempts(mut self, max: u32) -> Self {
166 self.max_attempts = max.max(1);
167 self
168 }
169
170 pub fn as_last_attempt(mut self) -> Self {
172 self.attempt = 3;
173 self.max_attempts = 3;
174 self
175 }
176
177 pub fn as_user(mut self, id: Uuid) -> Self {
179 self.user_id = Some(id);
180 self
181 }
182
183 pub fn as_subject(mut self, subject: impl Into<String>) -> Self {
185 self.claims
186 .insert("sub".to_string(), serde_json::json!(subject.into()));
187 self
188 }
189
190 pub fn with_role(mut self, role: impl Into<String>) -> Self {
192 self.roles.push(role.into());
193 self
194 }
195
196 pub fn with_pool(mut self, pool: PgPool) -> Self {
198 self.pool = Some(pool);
199 self
200 }
201
202 pub fn mock_http<F>(self, pattern: &str, handler: F) -> Self
204 where
205 F: Fn(&MockRequest) -> MockResponse + Send + Sync + 'static,
206 {
207 self.http.add_mock_sync(pattern, handler);
208 self
209 }
210
211 pub fn mock_http_json<T: serde::Serialize>(self, pattern: &str, response: T) -> Self {
213 let json = serde_json::to_value(response).unwrap_or(serde_json::Value::Null);
214 self.mock_http(pattern, move |_| MockResponse::json(json.clone()))
215 }
216
217 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
219 self.env_vars.insert(key.into(), value.into());
220 self
221 }
222
223 pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
225 self.env_vars.extend(vars);
226 self
227 }
228
229 pub fn build(self) -> TestJobContext {
231 TestJobContext {
232 job_id: self.job_id.unwrap_or_else(Uuid::new_v4),
233 job_type: self.job_type,
234 attempt: self.attempt,
235 max_attempts: self.max_attempts,
236 auth: build_test_auth(self.user_id, self.roles, self.claims),
237 pool: self.pool,
238 http: Arc::new(self.http),
239 progress_updates: Arc::new(RwLock::new(Vec::new())),
240 env_provider: Arc::new(MockEnvProvider::with_vars(self.env_vars)),
241 }
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_job_context_creation() {
251 let ctx = TestJobContext::builder("export_users").build();
252
253 assert_eq!(ctx.job_type, "export_users");
254 assert_eq!(ctx.attempt, 1);
255 assert!(!ctx.is_retry());
256 assert!(ctx.is_last_attempt()); }
258
259 #[test]
260 fn test_retry_detection() {
261 let ctx = TestJobContext::builder("test")
262 .as_retry(3)
263 .with_max_attempts(5)
264 .build();
265
266 assert!(ctx.is_retry());
267 assert!(!ctx.is_last_attempt());
268 }
269
270 #[test]
271 fn test_last_attempt() {
272 let ctx = TestJobContext::builder("test").as_last_attempt().build();
273
274 assert!(ctx.is_retry());
275 assert!(ctx.is_last_attempt());
276 }
277
278 #[test]
279 fn test_progress_tracking() {
280 let ctx = TestJobContext::builder("test").build();
281
282 ctx.progress(25, "Step 1 complete").unwrap();
283 ctx.progress(50, "Step 2 complete").unwrap();
284 ctx.progress(100, "Done").unwrap();
285
286 let updates = ctx.progress_updates();
287 assert_eq!(updates.len(), 3);
288 assert_eq!(updates[0].percent, 25);
289 assert_eq!(updates[2].percent, 100);
290 }
291}