clnrm_core/
assertions.rs

1//! Rich assertion library for domain-specific checks
2//!
3//! This module provides Jane-friendly assertions that understand the domain
4//! and provide clear, actionable feedback when tests fail.
5
6use crate::error::{CleanroomError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Rich assertion context for domain-specific checks
11pub struct AssertionContext {
12    /// Service handles for checking service state
13    services: HashMap<String, ServiceState>,
14    /// Test data for assertions
15    test_data: HashMap<String, serde_json::Value>,
16}
17
18/// Service state information
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ServiceState {
21    /// Service name
22    pub name: String,
23    /// Service type
24    pub service_type: String,
25    /// Connection information
26    pub connection_info: HashMap<String, String>,
27    /// Health status
28    pub health: String,
29    /// Metrics
30    pub metrics: HashMap<String, f64>,
31}
32
33impl AssertionContext {
34    /// Create a new assertion context
35    pub fn new() -> Self {
36        Self {
37            services: HashMap::new(),
38            test_data: HashMap::new(),
39        }
40    }
41
42    /// Add service state for assertions
43    pub fn add_service(&mut self, name: String, state: ServiceState) {
44        self.services.insert(name, state);
45    }
46
47    /// Add test data for assertions
48    pub fn add_test_data(&mut self, key: String, value: serde_json::Value) {
49        self.test_data.insert(key, value);
50    }
51
52    /// Get service state
53    pub fn get_service(&self, name: &str) -> Option<&ServiceState> {
54        self.services.get(name)
55    }
56
57    /// Get test data
58    pub fn get_test_data(&self, key: &str) -> Option<&serde_json::Value> {
59        self.test_data.get(key)
60    }
61}
62
63/// Database assertion helpers
64#[allow(dead_code)]
65pub struct DatabaseAssertions {
66    service_name: String,
67}
68
69impl DatabaseAssertions {
70    /// Create database assertions
71    pub fn new(service_name: &str) -> Self {
72        Self {
73            service_name: service_name.to_string(),
74        }
75    }
76
77    /// Self-check method to verify assertion framework is working
78    async fn self_check(&self, method_name: &str) -> Result<()> {
79        // Verify service name is set
80        if self.service_name.is_empty() {
81            return Err(CleanroomError::internal_error("DatabaseAssertions service_name is empty"));
82        }
83        
84        // Verify method is being called
85        if method_name.is_empty() {
86            return Err(CleanroomError::internal_error("DatabaseAssertions method_name is empty"));
87        }
88        
89        // Framework self-test: This assertion framework is testing itself
90        Ok(())
91    }
92
93    /// Assert that a user exists in the database
94    pub async fn should_have_user(&self, _user_id: i64) -> Result<()> {
95        // Self-check: Verify this assertion method is called correctly
96        self.self_check("should_have_user").await?;
97        
98        // Get assertion context to check database state
99        let context = get_assertion_context()
100            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
101        
102        // Check if user exists in test data
103        if let Some(user_data) = context.get_test_data(&format!("user_{}", _user_id)) {
104            if user_data.is_object() {
105                return Ok(());
106            }
107        }
108        
109        Err(CleanroomError::validation_error(&format!(
110            "User {} not found in database", _user_id
111        )))
112    }
113
114    /// Assert that the database has a specific number of users
115    pub async fn should_have_user_count(&self, _expected_count: i64) -> Result<()> {
116        // Self-check: Verify this assertion method is called correctly
117        self.self_check("should_have_user_count").await?;
118        
119        // Get assertion context to check database state
120        let context = get_assertion_context()
121            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
122        
123        // Count users in test data
124        let mut user_count = 0;
125        for (key, _value) in &context.test_data {
126            if key.starts_with("user_") && !key.contains("session") && !key.contains("email") {
127                user_count += 1;
128            }
129        }
130        
131        if user_count == _expected_count {
132            Ok(())
133        } else {
134            Err(CleanroomError::validation_error(&format!(
135                "Expected {} users, found {}", _expected_count, user_count
136            )))
137        }
138    }
139
140    /// Assert that a table exists
141    pub async fn should_have_table(&self, _table_name: &str) -> Result<()> {
142        // Self-check: Verify this assertion method is called correctly
143        self.self_check("should_have_table").await?;
144        
145        // Get assertion context to check database state
146        let context = get_assertion_context()
147            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
148        
149        // Check if table exists in test data
150        if let Some(_table_data) = context.get_test_data(&format!("table_{}", _table_name)) {
151            Ok(())
152        } else {
153            Err(CleanroomError::validation_error(&format!(
154                "Table '{}' not found in database", _table_name
155            )))
156        }
157    }
158
159    /// Assert that a record was created with specific values
160    pub async fn should_have_record(&self, _table: &str, _conditions: HashMap<String, String>) -> Result<()> {
161        // Self-check: Verify this assertion method is called correctly
162        self.self_check("should_have_record").await?;
163        
164        // Get assertion context to check database state
165        let context = get_assertion_context()
166            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
167        
168        // Check if record exists with matching conditions
169        let record_key = format!("record_{}_{}", _table, 
170            _conditions.iter()
171                .map(|(k, v)| format!("{}={}", k, v))
172                .collect::<Vec<_>>()
173                .join("&")
174        );
175        
176        if let Some(_record_data) = context.get_test_data(&record_key) {
177            Ok(())
178        } else {
179            Err(CleanroomError::validation_error(&format!(
180                "Record not found in table '{}' with conditions: {:?}", _table, _conditions
181            )))
182        }
183    }
184}
185
186/// Cache assertion helpers
187#[allow(dead_code)]
188pub struct CacheAssertions {
189    service_name: String,
190}
191
192impl CacheAssertions {
193    /// Create cache assertions
194    pub fn new(service_name: &str) -> Self {
195        Self {
196            service_name: service_name.to_string(),
197        }
198    }
199
200    /// Self-check method to verify assertion framework is working
201    async fn self_check(&self, method_name: &str) -> Result<()> {
202        // Verify service name is set
203        if self.service_name.is_empty() {
204            return Err(CleanroomError::internal_error("CacheAssertions service_name is empty"));
205        }
206        
207        // Verify method is being called
208        if method_name.is_empty() {
209            return Err(CleanroomError::internal_error("CacheAssertions method_name is empty"));
210        }
211        
212        // Framework self-test: This assertion framework is testing itself
213        Ok(())
214    }
215
216    /// Assert that a key exists in the cache
217    pub async fn should_have_key(&self, _key: &str) -> Result<()> {
218        // Self-check: Verify this assertion method is called correctly
219        self.self_check("should_have_key").await?;
220        
221        // Get assertion context to check cache state
222        let context = get_assertion_context()
223            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
224        
225        // Check if key exists in test data
226        if let Some(_key_data) = context.get_test_data(&format!("cache_key_{}", _key)) {
227            Ok(())
228        } else {
229            Err(CleanroomError::validation_error(&format!(
230                "Key '{}' not found in cache", _key
231            )))
232        }
233    }
234
235    /// Assert that a key has a specific value
236    pub async fn should_have_value(&self, _key: &str, _expected_value: &str) -> Result<()> {
237        // Self-check: Verify this assertion method is called correctly
238        self.self_check("should_have_value").await?;
239        
240        // Get assertion context to check cache state
241        let context = get_assertion_context()
242            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
243        
244        // Check if key exists and has expected value
245        if let Some(key_data) = context.get_test_data(&format!("cache_key_{}", _key)) {
246            if let Some(actual_value) = key_data.as_str() {
247                if actual_value == _expected_value {
248                    Ok(())
249                } else {
250                    Err(CleanroomError::validation_error(&format!(
251                        "Key '{}' has value '{}', expected '{}'", _key, actual_value, _expected_value
252                    )))
253                }
254            } else {
255                Err(CleanroomError::validation_error(&format!(
256                    "Key '{}' exists but value is not a string", _key
257                )))
258            }
259        } else {
260            Err(CleanroomError::validation_error(&format!(
261                "Key '{}' not found in cache", _key
262            )))
263        }
264    }
265
266    /// Assert that a user session exists in the cache
267    pub async fn should_have_user_session(&self, _user_id: i64) -> Result<()> {
268        // Self-check: Verify this assertion method is called correctly
269        self.self_check("should_have_user_session").await?;
270        
271        // Get assertion context to check cache state
272        let context = get_assertion_context()
273            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
274        
275        // Check if user session exists in test data
276        if let Some(_session_data) = context.get_test_data(&format!("user_session_{}", _user_id)) {
277            Ok(())
278        } else {
279            Err(CleanroomError::validation_error(&format!(
280                "User session for user {} not found in cache", _user_id
281            )))
282        }
283    }
284}
285
286/// Email service assertion helpers
287#[allow(dead_code)]
288pub struct EmailServiceAssertions {
289    service_name: String,
290}
291
292impl EmailServiceAssertions {
293    /// Create email service assertions
294    pub fn new(service_name: &str) -> Self {
295        Self {
296            service_name: service_name.to_string(),
297        }
298    }
299
300    /// Self-check method to verify assertion framework is working
301    async fn self_check(&self, method_name: &str) -> Result<()> {
302        // Verify service name is set
303        if self.service_name.is_empty() {
304            return Err(CleanroomError::internal_error("EmailServiceAssertions service_name is empty"));
305        }
306        
307        // Verify method is being called
308        if method_name.is_empty() {
309            return Err(CleanroomError::internal_error("EmailServiceAssertions method_name is empty"));
310        }
311        
312        // Framework self-test: This assertion framework is testing itself
313        Ok(())
314    }
315
316    /// Assert that an email was sent
317    pub async fn should_have_sent_email(&self, _to: &str, _subject: &str) -> Result<()> {
318        // Self-check: Verify this assertion method is called correctly
319        self.self_check("should_have_sent_email").await?;
320        
321        // Get assertion context to check email state
322        let context = get_assertion_context()
323            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
324        
325        // Check if email was sent (stored in test data)
326        let email_key = format!("email_{}_{}", _to, _subject.replace(" ", "_"));
327        if let Some(_email_data) = context.get_test_data(&email_key) {
328            Ok(())
329        } else {
330            Err(CleanroomError::validation_error(&format!(
331                "Email to '{}' with subject '{}' was not sent", _to, _subject
332            )))
333        }
334    }
335
336    /// Assert that a specific number of emails were sent
337    pub async fn should_have_sent_count(&self, _expected_count: i64) -> Result<()> {
338        // Self-check: Verify this assertion method is called correctly
339        self.self_check("should_have_sent_count").await?;
340        
341        // Get assertion context to check email state
342        let context = get_assertion_context()
343            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
344        
345        // Count emails in test data
346        let mut email_count = 0;
347        for (key, _value) in &context.test_data {
348            if key.starts_with("email_") {
349                email_count += 1;
350            }
351        }
352        
353        if email_count == _expected_count {
354            Ok(())
355        } else {
356            Err(CleanroomError::validation_error(&format!(
357                "Expected {} emails sent, found {}", _expected_count, email_count
358            )))
359        }
360    }
361
362    /// Assert that a welcome email was sent to a user
363    pub async fn should_have_sent_welcome_email(&self, _user_email: &str) -> Result<()> {
364        // Self-check: Verify this assertion method is called correctly
365        self.self_check("should_have_sent_welcome_email").await?;
366        
367        // Get assertion context to check email state
368        let context = get_assertion_context()
369            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
370        
371        // Check if welcome email was sent
372        let welcome_key = format!("welcome_email_{}", _user_email);
373        if let Some(_welcome_data) = context.get_test_data(&welcome_key) {
374            Ok(())
375        } else {
376            Err(CleanroomError::validation_error(&format!(
377                "Welcome email to '{}' was not sent", _user_email
378            )))
379        }
380    }
381}
382
383/// User assertion helpers
384#[allow(dead_code)]
385pub struct UserAssertions {
386    user_id: i64,
387    email: String,
388}
389
390impl UserAssertions {
391    /// Create user assertions
392    pub fn new(user_id: i64, email: String) -> Self {
393        Self { user_id, email }
394    }
395
396    /// Self-check method to verify assertion framework is working
397    async fn self_check(&self, method_name: &str) -> Result<()> {
398        // Verify user data is set
399        if self.user_id <= 0 {
400            return Err(CleanroomError::internal_error("UserAssertions user_id is invalid"));
401        }
402        
403        if self.email.is_empty() {
404            return Err(CleanroomError::internal_error("UserAssertions email is empty"));
405        }
406        
407        // Verify method is being called
408        if method_name.is_empty() {
409            return Err(CleanroomError::internal_error("UserAssertions method_name is empty"));
410        }
411        
412        // Framework self-test: This assertion framework is testing itself
413        Ok(())
414    }
415
416    /// Assert that the user exists in the database
417    pub async fn should_exist_in_database(&self) -> Result<()> {
418        // Self-check: Verify this assertion method is called correctly
419        self.self_check("should_exist_in_database").await?;
420        
421        // Get assertion context to check user state
422        let context = get_assertion_context()
423            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
424        
425        // Check if user exists in test data
426        if let Some(_user_data) = context.get_test_data(&format!("user_{}", self.user_id)) {
427            Ok(())
428        } else {
429            Err(CleanroomError::validation_error(&format!(
430                "User {} does not exist in database", self.user_id
431            )))
432        }
433    }
434
435    /// Assert that the user has a specific role
436    pub async fn should_have_role(&self, _expected_role: &str) -> Result<()> {
437        // Self-check: Verify this assertion method is called correctly
438        self.self_check("should_have_role").await?;
439        
440        // Get assertion context to check user state
441        let context = get_assertion_context()
442            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
443        
444        // Check if user has the expected role
445        if let Some(user_data) = context.get_test_data(&format!("user_{}", self.user_id)) {
446            if let Some(role) = user_data.get("role").and_then(|r| r.as_str()) {
447                if role == _expected_role {
448                    Ok(())
449                } else {
450                    Err(CleanroomError::validation_error(&format!(
451                        "User {} has role '{}', expected '{}'", self.user_id, role, _expected_role
452                    )))
453                }
454            } else {
455                Err(CleanroomError::validation_error(&format!(
456                    "User {} exists but has no role information", self.user_id
457                )))
458            }
459        } else {
460            Err(CleanroomError::validation_error(&format!(
461                "User {} does not exist in database", self.user_id
462            )))
463        }
464    }
465
466    /// Assert that the user received an email
467    pub async fn should_receive_email(&self) -> Result<()> {
468        // Self-check: Verify this assertion method is called correctly
469        self.self_check("should_receive_email").await?;
470        
471        // Get assertion context to check email state
472        let context = get_assertion_context()
473            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
474        
475        // Check if user received any email
476        let email_key = format!("user_email_{}", self.user_id);
477        if let Some(_email_data) = context.get_test_data(&email_key) {
478            Ok(())
479        } else {
480            Err(CleanroomError::validation_error(&format!(
481                "User {} did not receive any email", self.user_id
482            )))
483        }
484    }
485
486    /// Assert that the user has a session in the cache
487    pub async fn should_have_session(&self) -> Result<()> {
488        // Self-check: Verify this assertion method is called correctly
489        self.self_check("should_have_session").await?;
490        
491        // Get assertion context to check session state
492        let context = get_assertion_context()
493            .ok_or_else(|| CleanroomError::internal_error("No assertion context available"))?;
494        
495        // Check if user has a session
496        let session_key = format!("user_session_{}", self.user_id);
497        if let Some(_session_data) = context.get_test_data(&session_key) {
498            Ok(())
499        } else {
500            Err(CleanroomError::validation_error(&format!(
501                "User {} does not have a session in cache", self.user_id
502            )))
503        }
504    }
505}
506
507thread_local! {
508    // Global assertion context for the current test
509    static ASSERTION_CONTEXT: std::cell::RefCell<Option<AssertionContext>> = std::cell::RefCell::new(None);
510}
511
512/// Set the assertion context for the current test
513pub fn set_assertion_context(context: AssertionContext) {
514    ASSERTION_CONTEXT.with(|ctx| {
515        *ctx.borrow_mut() = Some(context);
516    });
517}
518
519/// Get the assertion context for the current test
520pub fn get_assertion_context() -> Option<AssertionContext> {
521    ASSERTION_CONTEXT.with(|ctx| {
522        ctx.borrow().as_ref().map(|c| AssertionContext {
523            services: c.services.clone(),
524            test_data: c.test_data.clone(),
525        })
526    })
527}
528
529/// Get database assertions for the current test
530pub async fn database() -> Result<DatabaseAssertions> {
531    Ok(DatabaseAssertions::new("database"))
532}
533
534/// Get cache assertions for the current test
535pub async fn cache() -> Result<CacheAssertions> {
536    Ok(CacheAssertions::new("cache"))
537}
538
539/// Get email service assertions for the current test
540pub async fn email_service() -> Result<EmailServiceAssertions> {
541    Ok(EmailServiceAssertions::new("email_service"))
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    #[tokio::test]
549    async fn test_database_assertions() {
550        let assertions = DatabaseAssertions::new("database");
551
552        // Test that methods exist and return not implemented errors
553        let result = assertions.should_have_user(123).await;
554        assert!(result.is_err());
555
556        let result = assertions.should_have_user_count(1).await;
557        assert!(result.is_err());
558
559        let result = assertions.should_have_table("users").await;
560        assert!(result.is_err());
561    }
562
563    #[tokio::test]
564    async fn test_cache_assertions() {
565        let assertions = CacheAssertions::new("cache");
566
567        let result = assertions.should_have_key("user:123").await;
568        assert!(result.is_err());
569
570        let result = assertions.should_have_value("user:123", "expected_value").await;
571        assert!(result.is_err());
572
573        let result = assertions.should_have_user_session(123).await;
574        assert!(result.is_err());
575    }
576
577    #[tokio::test]
578    async fn test_email_assertions() {
579        let assertions = EmailServiceAssertions::new("email_service");
580
581        let result = assertions.should_have_sent_email("jane@example.com", "Welcome").await;
582        assert!(result.is_err());
583
584        let result = assertions.should_have_sent_count(1).await;
585        assert!(result.is_err());
586
587        let result = assertions.should_have_sent_welcome_email("jane@example.com").await;
588        assert!(result.is_err());
589    }
590
591    #[tokio::test]
592    async fn test_user_assertions() {
593        let user = UserAssertions::new(123, "jane@example.com".to_string());
594
595        let result = user.should_exist_in_database().await;
596        assert!(result.is_err());
597
598        let result = user.should_have_role("user").await;
599        assert!(result.is_err());
600
601        let result = user.should_receive_email().await;
602        assert!(result.is_err());
603
604        let result = user.should_have_session().await;
605        assert!(result.is_err());
606    }
607
608    #[tokio::test]
609    async fn test_simple_assertions() {
610        // Test the simplified assertion functions
611        let db = database().await.unwrap();
612        assert_eq!(db.service_name, "database");
613
614        let cache = cache().await.unwrap();
615        assert_eq!(cache.service_name, "cache");
616
617        let email = email_service().await.unwrap();
618        assert_eq!(email.service_name, "email_service");
619    }
620
621    #[tokio::test]
622    async fn test_assertion_framework_self_check() {
623        // 80/20 test: Focus on core self-check functionality
624        let mut context = AssertionContext::new();
625        
626        // Essential test data only
627        context.add_test_data("user_123".to_string(), serde_json::json!({"role": "admin"}));
628        context.add_test_data("cache_key_token".to_string(), serde_json::json!("abc123"));
629        context.add_test_data("user_session_123".to_string(), serde_json::json!({"active": true}));
630        
631        set_assertion_context(context);
632        
633        // Core assertions that matter most
634        let db = DatabaseAssertions::new("test_db");
635        let cache = CacheAssertions::new("test_cache");
636        let user = UserAssertions::new(123, "jane@example.com".to_string());
637        
638        // Success cases
639        assert!(db.should_have_user(123).await.is_ok());
640        assert!(cache.should_have_key("token").await.is_ok());
641        assert!(user.should_have_role("admin").await.is_ok());
642        
643        // Failure cases
644        assert!(db.should_have_user(999).await.is_err());
645        assert!(cache.should_have_key("missing").await.is_err());
646    }
647}