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