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 crate::Result;
11use crate::env::{EnvAccess, EnvProvider, MockEnvProvider};
12use crate::function::{AuthContext, RequestMetadata};
13
14/// Test context for mutation functions.
15///
16/// Provides an isolated testing environment for mutations with configurable
17/// authentication, optional database access, and mock job/workflow dispatch.
18///
19/// # Example
20///
21/// ```ignore
22/// let ctx = TestMutationContext::builder()
23///     .as_user(Uuid::new_v4())
24///     .build();
25///
26/// // Dispatch a job
27/// ctx.dispatch_job("send_email", json!({"to": "test@example.com"})).await?;
28///
29/// // Verify job was dispatched
30/// ctx.job_dispatch().assert_dispatched("send_email");
31/// ```
32pub struct TestMutationContext {
33    /// Authentication context.
34    pub auth: AuthContext,
35    /// Request metadata.
36    pub request: RequestMetadata,
37    /// Optional database pool.
38    pool: Option<PgPool>,
39    /// Mock job dispatch for verification.
40    job_dispatch: Arc<MockJobDispatch>,
41    /// Mock workflow dispatch for verification.
42    workflow_dispatch: Arc<MockWorkflowDispatch>,
43    /// Mock environment provider.
44    env_provider: Arc<MockEnvProvider>,
45}
46
47impl TestMutationContext {
48    /// Create a new builder.
49    pub fn builder() -> TestMutationContextBuilder {
50        TestMutationContextBuilder::default()
51    }
52
53    /// Create a minimal unauthenticated context.
54    pub fn minimal() -> Self {
55        Self::builder().build()
56    }
57
58    /// Create an authenticated context.
59    pub fn authenticated(user_id: Uuid) -> Self {
60        Self::builder().as_user(user_id).build()
61    }
62
63    /// Get the database pool (if available).
64    pub fn db(&self) -> Option<&PgPool> {
65        self.pool.as_ref()
66    }
67
68    /// Get the mock job dispatch for verification.
69    pub fn job_dispatch(&self) -> &MockJobDispatch {
70        &self.job_dispatch
71    }
72
73    /// Get the mock workflow dispatch for verification.
74    pub fn workflow_dispatch(&self) -> &MockWorkflowDispatch {
75        &self.workflow_dispatch
76    }
77
78    /// Get the authenticated user ID or return an error.
79    pub fn require_user_id(&self) -> Result<Uuid> {
80        self.auth.require_user_id()
81    }
82
83    /// Dispatch a job (records for later verification).
84    pub async fn dispatch_job<T: serde::Serialize>(&self, job_type: &str, args: T) -> Result<Uuid> {
85        self.job_dispatch.dispatch(job_type, args).await
86    }
87
88    /// Start a workflow (records for later verification).
89    pub async fn start_workflow<T: serde::Serialize>(&self, name: &str, input: T) -> Result<Uuid> {
90        self.workflow_dispatch.start(name, input).await
91    }
92
93    /// Get the mock env provider for verification.
94    pub fn env_mock(&self) -> &MockEnvProvider {
95        &self.env_provider
96    }
97}
98
99impl EnvAccess for TestMutationContext {
100    fn env_provider(&self) -> &dyn EnvProvider {
101        self.env_provider.as_ref()
102    }
103}
104
105/// Builder for TestMutationContext.
106pub struct TestMutationContextBuilder {
107    user_id: Option<Uuid>,
108    roles: Vec<String>,
109    claims: HashMap<String, serde_json::Value>,
110    pool: Option<PgPool>,
111    job_dispatch: Arc<MockJobDispatch>,
112    workflow_dispatch: Arc<MockWorkflowDispatch>,
113    env_vars: HashMap<String, String>,
114}
115
116impl Default for TestMutationContextBuilder {
117    fn default() -> Self {
118        Self {
119            user_id: None,
120            roles: Vec::new(),
121            claims: HashMap::new(),
122            pool: None,
123            job_dispatch: Arc::new(MockJobDispatch::new()),
124            workflow_dispatch: Arc::new(MockWorkflowDispatch::new()),
125            env_vars: HashMap::new(),
126        }
127    }
128}
129
130impl TestMutationContextBuilder {
131    /// Set the authenticated user.
132    pub fn as_user(mut self, id: Uuid) -> Self {
133        self.user_id = Some(id);
134        self
135    }
136
137    /// Add a role.
138    pub fn with_role(mut self, role: impl Into<String>) -> Self {
139        self.roles.push(role.into());
140        self
141    }
142
143    /// Add multiple roles.
144    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
145        self.roles.extend(roles);
146        self
147    }
148
149    /// Add a custom claim.
150    pub fn with_claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
151        self.claims.insert(key.into(), value);
152        self
153    }
154
155    /// Set the database pool.
156    pub fn with_pool(mut self, pool: PgPool) -> Self {
157        self.pool = Some(pool);
158        self
159    }
160
161    /// Use a specific mock job dispatch.
162    pub fn with_job_dispatch(mut self, dispatch: Arc<MockJobDispatch>) -> Self {
163        self.job_dispatch = dispatch;
164        self
165    }
166
167    /// Use a specific mock workflow dispatch.
168    pub fn with_workflow_dispatch(mut self, dispatch: Arc<MockWorkflowDispatch>) -> Self {
169        self.workflow_dispatch = dispatch;
170        self
171    }
172
173    /// Set a single environment variable.
174    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
175        self.env_vars.insert(key.into(), value.into());
176        self
177    }
178
179    /// Set multiple environment variables.
180    pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
181        self.env_vars.extend(vars);
182        self
183    }
184
185    /// Build the test context.
186    pub fn build(self) -> TestMutationContext {
187        let auth = if let Some(user_id) = self.user_id {
188            AuthContext::authenticated(user_id, self.roles, self.claims)
189        } else {
190            AuthContext::unauthenticated()
191        };
192
193        TestMutationContext {
194            auth,
195            request: RequestMetadata::default(),
196            pool: self.pool,
197            job_dispatch: self.job_dispatch,
198            workflow_dispatch: self.workflow_dispatch,
199            env_provider: Arc::new(MockEnvProvider::with_vars(self.env_vars)),
200        }
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[tokio::test]
209    async fn test_dispatch_job() {
210        let ctx = TestMutationContext::authenticated(Uuid::new_v4());
211
212        let job_id = ctx
213            .dispatch_job("send_email", serde_json::json!({"to": "test@example.com"}))
214            .await
215            .unwrap();
216
217        assert!(!job_id.is_nil());
218        ctx.job_dispatch().assert_dispatched("send_email");
219    }
220
221    #[tokio::test]
222    async fn test_start_workflow() {
223        let ctx = TestMutationContext::authenticated(Uuid::new_v4());
224
225        let run_id = ctx
226            .start_workflow("onboarding", serde_json::json!({"user_id": "123"}))
227            .await
228            .unwrap();
229
230        assert!(!run_id.is_nil());
231        ctx.workflow_dispatch().assert_started("onboarding");
232    }
233
234    #[tokio::test]
235    async fn test_job_not_dispatched() {
236        let ctx = TestMutationContext::authenticated(Uuid::new_v4());
237
238        ctx.job_dispatch().assert_not_dispatched("send_email");
239    }
240}