forge_core/testing/context/
action.rs

1//! Test context for action 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 crate::Result;
12use crate::env::{EnvAccess, EnvProvider, MockEnvProvider};
13use crate::function::{AuthContext, RequestMetadata};
14
15/// Test context for action functions.
16///
17/// Provides an isolated testing environment for actions with HTTP mocking,
18/// configurable authentication, and optional database access.
19///
20/// # Example
21///
22/// ```ignore
23/// let ctx = TestActionContext::builder()
24///     .as_user(Uuid::new_v4())
25///     .mock_http_json("https://api.example.com/*", json!({"status": "ok"}))
26///     .build();
27///
28/// // Make HTTP call (will return mocked response)
29/// let response = ctx.http().execute(...).await;
30///
31/// // Verify HTTP call was made
32/// ctx.http().assert_called("https://api.example.com/*");
33/// ```
34pub struct TestActionContext {
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.
44    job_dispatch: Arc<MockJobDispatch>,
45    /// Mock workflow dispatch.
46    workflow_dispatch: Arc<MockWorkflowDispatch>,
47    /// Mock environment provider.
48    env_provider: Arc<MockEnvProvider>,
49}
50
51impl TestActionContext {
52    /// Create a new builder.
53    pub fn builder() -> TestActionContextBuilder {
54        TestActionContextBuilder::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    /// Dispatch a job (records for later verification).
93    pub async fn dispatch_job<T: serde::Serialize>(&self, job_type: &str, args: T) -> Result<Uuid> {
94        self.job_dispatch.dispatch(job_type, args).await
95    }
96
97    /// Start a workflow (records for later verification).
98    pub async fn start_workflow<T: serde::Serialize>(&self, name: &str, input: T) -> Result<Uuid> {
99        self.workflow_dispatch.start(name, input).await
100    }
101
102    /// Get the mock env provider for verification.
103    pub fn env_mock(&self) -> &MockEnvProvider {
104        &self.env_provider
105    }
106}
107
108impl EnvAccess for TestActionContext {
109    fn env_provider(&self) -> &dyn EnvProvider {
110        self.env_provider.as_ref()
111    }
112}
113
114/// Builder for TestActionContext.
115pub struct TestActionContextBuilder {
116    user_id: Option<Uuid>,
117    roles: Vec<String>,
118    claims: HashMap<String, serde_json::Value>,
119    pool: Option<PgPool>,
120    http: MockHttp,
121    job_dispatch: Arc<MockJobDispatch>,
122    workflow_dispatch: Arc<MockWorkflowDispatch>,
123    env_vars: HashMap<String, String>,
124}
125
126impl Default for TestActionContextBuilder {
127    fn default() -> Self {
128        Self {
129            user_id: None,
130            roles: Vec::new(),
131            claims: HashMap::new(),
132            pool: None,
133            http: MockHttp::new(),
134            job_dispatch: Arc::new(MockJobDispatch::new()),
135            workflow_dispatch: Arc::new(MockWorkflowDispatch::new()),
136            env_vars: HashMap::new(),
137        }
138    }
139}
140
141impl TestActionContextBuilder {
142    /// Set the authenticated user.
143    pub fn as_user(mut self, id: Uuid) -> Self {
144        self.user_id = Some(id);
145        self
146    }
147
148    /// Add a role.
149    pub fn with_role(mut self, role: impl Into<String>) -> Self {
150        self.roles.push(role.into());
151        self
152    }
153
154    /// Add multiple roles.
155    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
156        self.roles.extend(roles);
157        self
158    }
159
160    /// Add a custom claim.
161    pub fn with_claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
162        self.claims.insert(key.into(), value);
163        self
164    }
165
166    /// Set the database pool.
167    pub fn with_pool(mut self, pool: PgPool) -> Self {
168        self.pool = Some(pool);
169        self
170    }
171
172    /// Add an HTTP mock with a custom handler.
173    pub fn mock_http<F>(self, pattern: &str, handler: F) -> Self
174    where
175        F: Fn(&MockRequest) -> MockResponse + Send + Sync + 'static,
176    {
177        self.http.add_mock_sync(pattern, handler);
178        self
179    }
180
181    /// Add an HTTP mock that returns a JSON response.
182    pub fn mock_http_json<T: serde::Serialize>(self, pattern: &str, response: T) -> Self {
183        let json = serde_json::to_value(response).unwrap_or(serde_json::Value::Null);
184        self.mock_http(pattern, move |_| MockResponse::json(json.clone()))
185    }
186
187    /// Set a single environment variable.
188    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
189        self.env_vars.insert(key.into(), value.into());
190        self
191    }
192
193    /// Set multiple environment variables.
194    pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
195        self.env_vars.extend(vars);
196        self
197    }
198
199    /// Build the test context.
200    pub fn build(self) -> TestActionContext {
201        let auth = if let Some(user_id) = self.user_id {
202            AuthContext::authenticated(user_id, self.roles, self.claims)
203        } else {
204            AuthContext::unauthenticated()
205        };
206
207        TestActionContext {
208            auth,
209            request: RequestMetadata::default(),
210            pool: self.pool,
211            http: Arc::new(self.http),
212            job_dispatch: self.job_dispatch,
213            workflow_dispatch: self.workflow_dispatch,
214            env_provider: Arc::new(MockEnvProvider::with_vars(self.env_vars)),
215        }
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_minimal_context() {
225        let ctx = TestActionContext::minimal();
226        assert!(!ctx.auth.is_authenticated());
227    }
228
229    #[test]
230    fn test_authenticated_context() {
231        let user_id = Uuid::new_v4();
232        let ctx = TestActionContext::authenticated(user_id);
233        assert!(ctx.auth.is_authenticated());
234        assert_eq!(ctx.require_user_id().unwrap(), user_id);
235    }
236}