forge_core/testing/context/
mutation.rs

1//! Test context for mutation functions.
2
3use 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
16/// Test context for mutation functions.
17///
18/// Provides an isolated testing environment for mutations with configurable
19/// authentication, optional database access, and mock job/workflow dispatch.
20///
21/// # Example
22///
23/// ```ignore
24/// let ctx = TestMutationContext::builder()
25///     .as_user(Uuid::new_v4())
26///     .build();
27///
28/// // Dispatch a job
29/// ctx.dispatch_job("send_email", json!({"to": "test@example.com"})).await?;
30///
31/// // Verify job was dispatched
32/// ctx.job_dispatch().assert_dispatched("send_email");
33/// ```
34pub struct TestMutationContext {
35    /// Authentication context.
36    pub auth: AuthContext,
37    /// Request metadata.
38    pub request: RequestMetadata,
39    /// Optional database pool.
40    pool: Option<PgPool>,
41    /// Mock HTTP client.
42    http: Arc<MockHttp>,
43    /// Mock job dispatch for verification.
44    job_dispatch: Arc<MockJobDispatch>,
45    /// Mock workflow dispatch for verification.
46    workflow_dispatch: Arc<MockWorkflowDispatch>,
47    /// Mock environment provider.
48    env_provider: Arc<MockEnvProvider>,
49}
50
51impl TestMutationContext {
52    /// Create a new builder.
53    pub fn builder() -> TestMutationContextBuilder {
54        TestMutationContextBuilder::default()
55    }
56
57    /// Create a minimal unauthenticated context.
58    pub fn minimal() -> Self {
59        Self::builder().build()
60    }
61
62    /// Create an authenticated context.
63    pub fn authenticated(user_id: Uuid) -> Self {
64        Self::builder().as_user(user_id).build()
65    }
66
67    /// Get the database pool (if available).
68    pub fn db(&self) -> Option<&PgPool> {
69        self.pool.as_ref()
70    }
71
72    /// Get the mock HTTP client.
73    pub fn http(&self) -> &MockHttp {
74        &self.http
75    }
76
77    /// Get the mock job dispatch for verification.
78    pub fn job_dispatch(&self) -> &MockJobDispatch {
79        &self.job_dispatch
80    }
81
82    /// Get the mock workflow dispatch for verification.
83    pub fn workflow_dispatch(&self) -> &MockWorkflowDispatch {
84        &self.workflow_dispatch
85    }
86
87    /// Get the authenticated user ID or return an error.
88    pub fn require_user_id(&self) -> Result<Uuid> {
89        self.auth.require_user_id()
90    }
91
92    /// Like `require_user_id()` but for non-UUID auth providers.
93    pub fn require_subject(&self) -> Result<&str> {
94        self.auth.require_subject()
95    }
96
97    /// Dispatch a job (records for later verification).
98    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    /// Start a workflow (records for later verification).
103    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    /// Get the mock env provider for verification.
108    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
119/// Builder for TestMutationContext.
120pub 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    /// Set the authenticated user with a UUID.
148    pub fn as_user(mut self, id: Uuid) -> Self {
149        self.user_id = Some(id);
150        self
151    }
152
153    /// For non-UUID auth providers (Firebase, Clerk, etc.).
154    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    /// Add a role.
161    pub fn with_role(mut self, role: impl Into<String>) -> Self {
162        self.roles.push(role.into());
163        self
164    }
165
166    /// Add multiple roles.
167    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
168        self.roles.extend(roles);
169        self
170    }
171
172    /// Add a custom claim.
173    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    /// Set the database pool.
179    pub fn with_pool(mut self, pool: PgPool) -> Self {
180        self.pool = Some(pool);
181        self
182    }
183
184    /// Add an HTTP mock with a custom handler.
185    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    /// Add an HTTP mock that returns a JSON response.
194    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    /// Use a specific mock job dispatch.
200    pub fn with_job_dispatch(mut self, dispatch: Arc<MockJobDispatch>) -> Self {
201        self.job_dispatch = dispatch;
202        self
203    }
204
205    /// Use a specific mock workflow dispatch.
206    pub fn with_workflow_dispatch(mut self, dispatch: Arc<MockWorkflowDispatch>) -> Self {
207        self.workflow_dispatch = dispatch;
208        self
209    }
210
211    /// Set a single environment variable.
212    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    /// Set multiple environment variables.
218    pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
219        self.env_vars.extend(vars);
220        self
221    }
222
223    /// Build the test context.
224    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}