Skip to main content

forge_core/testing/context/
query.rs

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