forge_core/testing/context/
query.rs

1//! Test context for query functions.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use sqlx::PgPool;
7use uuid::Uuid;
8
9use crate::Result;
10use crate::env::{EnvAccess, EnvProvider, MockEnvProvider};
11use crate::function::{AuthContext, RequestMetadata};
12
13/// Test context for query functions.
14///
15/// Provides an isolated testing environment for queries with configurable
16/// authentication and optional database access.
17///
18/// # Example
19///
20/// ```ignore
21/// // Simple authenticated context
22/// let ctx = TestQueryContext::authenticated(Uuid::new_v4());
23///
24/// // Context with roles and claims
25/// let ctx = TestQueryContext::builder()
26///     .as_user(Uuid::new_v4())
27///     .with_role("admin")
28///     .with_claim("org_id", json!("org-123"))
29///     .build();
30/// ```
31pub struct TestQueryContext {
32    /// Authentication context.
33    pub auth: AuthContext,
34    /// Request metadata.
35    pub request: RequestMetadata,
36    /// Optional database pool for integration tests.
37    pool: Option<PgPool>,
38    /// Tenant ID for multi-tenant testing.
39    tenant_id: Option<Uuid>,
40    /// Mock environment provider.
41    env_provider: Arc<MockEnvProvider>,
42}
43
44impl TestQueryContext {
45    /// Create a new builder.
46    pub fn builder() -> TestQueryContextBuilder {
47        TestQueryContextBuilder::default()
48    }
49
50    /// Create a minimal unauthenticated context (no database).
51    pub fn minimal() -> Self {
52        Self::builder().build()
53    }
54
55    /// Create an authenticated context with the given user ID (no database).
56    pub fn authenticated(user_id: Uuid) -> Self {
57        Self::builder().as_user(user_id).build()
58    }
59
60    /// Create a context with a database pool.
61    pub fn with_pool(pool: PgPool, user_id: Option<Uuid>) -> Self {
62        let mut builder = Self::builder().with_pool(pool);
63        if let Some(id) = user_id {
64            builder = builder.as_user(id);
65        }
66        builder.build()
67    }
68
69    /// Get the database pool (if available).
70    pub fn db(&self) -> Option<&PgPool> {
71        self.pool.as_ref()
72    }
73
74    /// Get the authenticated user ID or return an error.
75    pub fn require_user_id(&self) -> Result<Uuid> {
76        self.auth.require_user_id()
77    }
78
79    /// Check if a specific role is present.
80    pub fn has_role(&self, role: &str) -> bool {
81        self.auth.has_role(role)
82    }
83
84    /// Get a claim value.
85    pub fn claim(&self, key: &str) -> Option<&serde_json::Value> {
86        self.auth.claim(key)
87    }
88
89    /// Get the tenant ID (if set).
90    pub fn tenant_id(&self) -> Option<Uuid> {
91        self.tenant_id
92    }
93
94    /// Get the mock env provider for verification.
95    pub fn env_mock(&self) -> &MockEnvProvider {
96        &self.env_provider
97    }
98}
99
100impl EnvAccess for TestQueryContext {
101    fn env_provider(&self) -> &dyn EnvProvider {
102        self.env_provider.as_ref()
103    }
104}
105
106/// Builder for TestQueryContext.
107#[derive(Default)]
108pub struct TestQueryContextBuilder {
109    user_id: Option<Uuid>,
110    roles: Vec<String>,
111    claims: HashMap<String, serde_json::Value>,
112    tenant_id: Option<Uuid>,
113    pool: Option<PgPool>,
114    env_vars: HashMap<String, String>,
115}
116
117impl TestQueryContextBuilder {
118    /// Set the authenticated user.
119    pub fn as_user(mut self, id: Uuid) -> Self {
120        self.user_id = Some(id);
121        self
122    }
123
124    /// Add a role.
125    pub fn with_role(mut self, role: impl Into<String>) -> Self {
126        self.roles.push(role.into());
127        self
128    }
129
130    /// Add multiple roles.
131    pub fn with_roles(mut self, roles: Vec<String>) -> Self {
132        self.roles.extend(roles);
133        self
134    }
135
136    /// Add a custom claim.
137    pub fn with_claim(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
138        self.claims.insert(key.into(), value);
139        self
140    }
141
142    /// Set the tenant ID for multi-tenant testing.
143    pub fn with_tenant(mut self, tenant_id: Uuid) -> Self {
144        self.tenant_id = Some(tenant_id);
145        self
146    }
147
148    /// Set the database pool.
149    pub fn with_pool(mut self, pool: PgPool) -> Self {
150        self.pool = Some(pool);
151        self
152    }
153
154    /// Set a single environment variable.
155    pub fn with_env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
156        self.env_vars.insert(key.into(), value.into());
157        self
158    }
159
160    /// Set multiple environment variables.
161    pub fn with_envs(mut self, vars: HashMap<String, String>) -> Self {
162        self.env_vars.extend(vars);
163        self
164    }
165
166    /// Build the test context.
167    pub fn build(self) -> TestQueryContext {
168        let auth = if let Some(user_id) = self.user_id {
169            AuthContext::authenticated(user_id, self.roles, self.claims)
170        } else {
171            AuthContext::unauthenticated()
172        };
173
174        TestQueryContext {
175            auth,
176            request: RequestMetadata::default(),
177            pool: self.pool,
178            tenant_id: self.tenant_id,
179            env_provider: Arc::new(MockEnvProvider::with_vars(self.env_vars)),
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187
188    #[test]
189    fn test_minimal_context() {
190        let ctx = TestQueryContext::minimal();
191        assert!(!ctx.auth.is_authenticated());
192        assert!(ctx.db().is_none());
193    }
194
195    #[test]
196    fn test_authenticated_context() {
197        let user_id = Uuid::new_v4();
198        let ctx = TestQueryContext::authenticated(user_id);
199        assert!(ctx.auth.is_authenticated());
200        assert_eq!(ctx.require_user_id().unwrap(), user_id);
201    }
202
203    #[test]
204    fn test_context_with_roles() {
205        let ctx = TestQueryContext::builder()
206            .as_user(Uuid::new_v4())
207            .with_role("admin")
208            .with_role("user")
209            .build();
210
211        assert!(ctx.has_role("admin"));
212        assert!(ctx.has_role("user"));
213        assert!(!ctx.has_role("superuser"));
214    }
215
216    #[test]
217    fn test_context_with_claims() {
218        let ctx = TestQueryContext::builder()
219            .as_user(Uuid::new_v4())
220            .with_claim("org_id", serde_json::json!("org-123"))
221            .build();
222
223        assert_eq!(ctx.claim("org_id"), Some(&serde_json::json!("org-123")));
224        assert!(ctx.claim("nonexistent").is_none());
225    }
226
227    #[test]
228    fn test_context_with_env() {
229        let ctx = TestQueryContext::builder()
230            .with_env("API_KEY", "test_key_123")
231            .with_env("TIMEOUT", "30")
232            .build();
233
234        // Test env access via EnvAccess trait
235        assert_eq!(ctx.env("API_KEY"), Some("test_key_123".to_string()));
236        assert_eq!(ctx.env_or("TIMEOUT", "10"), "30");
237        assert_eq!(ctx.env_or("MISSING", "default"), "default");
238
239        // Test env_require
240        assert!(ctx.env_require("API_KEY").is_ok());
241        assert!(ctx.env_require("MISSING").is_err());
242
243        // Test env_parse
244        let timeout: u32 = ctx.env_parse("TIMEOUT").unwrap();
245        assert_eq!(timeout, 30);
246
247        // Verify access tracking
248        ctx.env_mock().assert_accessed("API_KEY");
249        ctx.env_mock().assert_accessed("TIMEOUT");
250    }
251}