forge_core/testing/context/
mutation.rs1use std::collections::HashMap;
4use std::sync::Arc;
5
6use sqlx::PgPool;
7use uuid::Uuid;
8
9use super::super::mock_dispatch::{MockJobDispatch, MockWorkflowDispatch};
10use super::super::mock_http::{MockHttp, MockRequest, MockResponse};
11use super::build_test_auth;
12use crate::Result;
13use crate::env::{EnvAccess, EnvProvider, MockEnvProvider};
14use crate::function::{AuthContext, RequestMetadata};
15
16pub struct TestMutationContext {
35 pub auth: AuthContext,
37 pub request: RequestMetadata,
39 pool: Option<PgPool>,
41 http: Arc<MockHttp>,
43 job_dispatch: Arc<MockJobDispatch>,
45 workflow_dispatch: Arc<MockWorkflowDispatch>,
47 env_provider: Arc<MockEnvProvider>,
49}
50
51impl TestMutationContext {
52 pub fn builder() -> TestMutationContextBuilder {
54 TestMutationContextBuilder::default()
55 }
56
57 pub fn minimal() -> Self {
59 Self::builder().build()
60 }
61
62 pub fn authenticated(user_id: Uuid) -> Self {
64 Self::builder().as_user(user_id).build()
65 }
66
67 pub fn db(&self) -> Option<&PgPool> {
69 self.pool.as_ref()
70 }
71
72 pub fn http(&self) -> &MockHttp {
74 &self.http
75 }
76
77 pub fn job_dispatch(&self) -> &MockJobDispatch {
79 &self.job_dispatch
80 }
81
82 pub fn workflow_dispatch(&self) -> &MockWorkflowDispatch {
84 &self.workflow_dispatch
85 }
86
87 pub fn require_user_id(&self) -> Result<Uuid> {
89 self.auth.require_user_id()
90 }
91
92 pub fn require_subject(&self) -> Result<&str> {
94 self.auth.require_subject()
95 }
96
97 pub async fn dispatch_job<T: serde::Serialize>(&self, job_type: &str, args: T) -> Result<Uuid> {
99 self.job_dispatch.dispatch(job_type, args).await
100 }
101
102 pub async fn start_workflow<T: serde::Serialize>(&self, name: &str, input: T) -> Result<Uuid> {
104 self.workflow_dispatch.start(name, input).await
105 }
106
107 pub fn env_mock(&self) -> &MockEnvProvider {
109 &self.env_provider
110 }
111}
112
113impl EnvAccess for TestMutationContext {
114 fn env_provider(&self) -> &dyn EnvProvider {
115 self.env_provider.as_ref()
116 }
117}
118
119pub struct TestMutationContextBuilder {
121 user_id: Option<Uuid>,
122 roles: Vec<String>,
123 claims: HashMap<String, serde_json::Value>,
124 pool: Option<PgPool>,
125 http: MockHttp,
126 job_dispatch: Arc<MockJobDispatch>,
127 workflow_dispatch: Arc<MockWorkflowDispatch>,
128 env_vars: HashMap<String, String>,
129}
130
131impl Default for TestMutationContextBuilder {
132 fn default() -> Self {
133 Self {
134 user_id: None,
135 roles: Vec::new(),
136 claims: HashMap::new(),
137 pool: None,
138 http: MockHttp::new(),
139 job_dispatch: Arc::new(MockJobDispatch::new()),
140 workflow_dispatch: Arc::new(MockWorkflowDispatch::new()),
141 env_vars: HashMap::new(),
142 }
143 }
144}
145
146impl TestMutationContextBuilder {
147 pub fn as_user(mut self, id: Uuid) -> Self {
149 self.user_id = Some(id);
150 self
151 }
152
153 pub fn as_subject(mut self, subject: impl Into<String>) -> Self {
155 self.claims
156 .insert("sub".to_string(), serde_json::json!(subject.into()));
157 self
158 }
159
160 pub fn with_role(mut self, role: impl Into<String>) -> Self {
162 self.roles.push(role.into());
163 self
164 }
165
166 pub fn with_roles(mut self, roles: Vec<String>) -> Self {
168 self.roles.extend(roles);
169 self
170 }
171
172 pub fn with_claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
174 self.claims.insert(key.into(), value);
175 self
176 }
177
178 pub fn with_pool(mut self, pool: PgPool) -> Self {
180 self.pool = Some(pool);
181 self
182 }
183
184 pub fn mock_http<F>(self, pattern: &str, handler: F) -> Self
186 where
187 F: Fn(&MockRequest) -> MockResponse + Send + Sync + 'static,
188 {
189 self.http.add_mock_sync(pattern, handler);
190 self
191 }
192
193 pub fn mock_http_json<T: serde::Serialize>(self, pattern: &str, response: T) -> Self {
195 let json = serde_json::to_value(response).unwrap_or(serde_json::Value::Null);
196 self.mock_http(pattern, move |_| MockResponse::json(json.clone()))
197 }
198
199 pub fn with_job_dispatch(mut self, dispatch: Arc<MockJobDispatch>) -> Self {
201 self.job_dispatch = dispatch;
202 self
203 }
204
205 pub fn with_workflow_dispatch(mut self, dispatch: Arc<MockWorkflowDispatch>) -> Self {
207 self.workflow_dispatch = dispatch;
208 self
209 }
210
211 pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
213 self.env_vars.insert(key.into(), value.into());
214 self
215 }
216
217 pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
219 self.env_vars.extend(vars);
220 self
221 }
222
223 pub fn build(self) -> TestMutationContext {
225 TestMutationContext {
226 auth: build_test_auth(self.user_id, self.roles, self.claims),
227 request: RequestMetadata::default(),
228 pool: self.pool,
229 http: Arc::new(self.http),
230 job_dispatch: self.job_dispatch,
231 workflow_dispatch: self.workflow_dispatch,
232 env_provider: Arc::new(MockEnvProvider::with_vars(self.env_vars)),
233 }
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[tokio::test]
242 async fn test_dispatch_job() {
243 let ctx = TestMutationContext::authenticated(Uuid::new_v4());
244
245 let job_id = ctx
246 .dispatch_job("send_email", serde_json::json!({"to": "test@example.com"}))
247 .await
248 .unwrap();
249
250 assert!(!job_id.is_nil());
251 ctx.job_dispatch().assert_dispatched("send_email");
252 }
253
254 #[tokio::test]
255 async fn test_start_workflow() {
256 let ctx = TestMutationContext::authenticated(Uuid::new_v4());
257
258 let run_id = ctx
259 .start_workflow("onboarding", serde_json::json!({"user_id": "123"}))
260 .await
261 .unwrap();
262
263 assert!(!run_id.is_nil());
264 ctx.workflow_dispatch().assert_started("onboarding");
265 }
266
267 #[tokio::test]
268 async fn test_job_not_dispatched() {
269 let ctx = TestMutationContext::authenticated(Uuid::new_v4());
270
271 ctx.job_dispatch().assert_not_dispatched("send_email");
272 }
273}