# General Bots Testing Strategy **Version:** 6.1.0 **Purpose:** Comprehensive testing strategy for the General Bots platform --- ## Table of Contents 1. [Overview](#overview) 2. [Test Architecture](#test-architecture) 3. [Test Categories](#test-categories) 4. [Test Accounts Setup](#test-accounts-setup) 5. [Email Testing](#email-testing) 6. [Calendar & Meeting Testing](#calendar--meeting-testing) 7. [Drive Testing](#drive-testing) 8. [Bot Response Testing](#bot-response-testing) 9. [Integration Testing](#integration-testing) 10. [Load & Performance Testing](#load--performance-testing) 11. [CI/CD Pipeline](#cicd-pipeline) 12. [Test Data Management](#test-data-management) --- ## Overview ### Testing Philosophy Given the platform's scale and complexity (Chat, Mail, Drive, Meet, Tasks, Calendar, Analytics), we adopt a **layered testing approach**: ``` ┌─────────────────────────────────────────────────────────────┐ │ E2E Tests (10%) │ │ Full user journeys across apps │ ├─────────────────────────────────────────────────────────────┤ │ Integration Tests (30%) │ │ Cross-service communication, APIs │ ├─────────────────────────────────────────────────────────────┤ │ Unit Tests (60%) │ │ Individual functions, modules │ └─────────────────────────────────────────────────────────────┘ ``` ### Key Principles 1. **Isolated Test Environments** - Each test run gets fresh state 2. **Real Service Testing** - Test against actual Stalwart, PostgreSQL, MinIO instances 3. **Deterministic Results** - Tests must be reproducible 4. **Fast Feedback** - Unit tests < 100ms, Integration < 5s, E2E < 30s 5. **Test Data Cleanup** - Always clean up after tests --- ## Test Architecture ### Directory Structure ``` botserver/ ├── tests/ │ ├── unit/ │ │ ├── basic/ # BASIC interpreter tests │ │ ├── email/ # Email parsing, formatting │ │ ├── drive/ # File operations │ │ └── llm/ # LLM integration tests │ ├── integration/ │ │ ├── email/ # Email send/receive │ │ ├── calendar/ # Event CRUD, invites │ │ ├── meet/ # Video meeting lifecycle │ │ ├── drive/ # File sharing, sync │ │ └── bot/ # Bot responses │ ├── e2e/ │ │ ├── scenarios/ # Full user journeys │ │ └── smoke/ # Quick sanity checks │ ├── fixtures/ │ │ ├── emails/ # Sample email files │ │ ├── documents/ # Test documents │ │ └── responses/ # Expected LLM responses │ ├── helpers/ │ │ ├── test_accounts.rs │ │ ├── email_client.rs │ │ ├── calendar_client.rs │ │ └── assertions.rs │ └── common/ │ └── mod.rs # Shared test utilities ``` ### Test Configuration ```toml # tests/test_config.toml [test_environment] database_url = "postgresql://test:test@localhost:5433/gb_test" stalwart_url = "http://localhost:8080" minio_endpoint = "http://localhost:9001" livekit_url = "ws://localhost:7880" [test_accounts] sender_email = "sender@test.gb.local" receiver_email = "receiver@test.gb.local" bot_email = "bot@test.gb.local" admin_email = "admin@test.gb.local" [timeouts] email_delivery_ms = 5000 meeting_join_ms = 10000 bot_response_ms = 30000 ``` --- ## Test Categories ### 1. Unit Tests Fast, isolated tests for individual functions. ```rust // tests/unit/email/signature_test.rs #[cfg(test)] mod tests { use botserver::email::signature::*; #[test] fn test_append_global_signature() { let body = "Hello, World!"; let signature = "

--
General Bots Team

"; let result = append_signature(body, signature, SignaturePosition::Bottom); assert!(result.contains("Hello, World!")); assert!(result.contains("General Bots Team")); assert!(result.find("Hello").unwrap() < result.find("General Bots").unwrap()); } #[test] fn test_signature_with_user_override() { let global_sig = "Global Signature"; let user_sig = "User Signature"; let result = combine_signatures(global_sig, Some(user_sig)); // User signature should appear, global should be appended assert!(result.contains("User Signature")); assert!(result.contains("Global Signature")); } #[test] fn test_plain_text_signature_conversion() { let html_sig = "

John Doe
CEO

"; let plain = html_to_plain_signature(html_sig); assert_eq!(plain, "John Doe\nCEO"); } } ``` ### 2. Integration Tests Test communication between services. ```rust // tests/integration/email/send_receive_test.rs #[tokio::test] async fn test_email_send_and_receive() { let ctx = TestContext::new().await; // Create test accounts let sender = ctx.create_test_account("sender").await; let receiver = ctx.create_test_account("receiver").await; // Send email let email = EmailBuilder::new() .from(&sender.email) .to(&receiver.email) .subject("Integration Test Email") .body_html("

Test content

") .build(); ctx.email_service.send(email).await.unwrap(); // Wait for delivery (max 5 seconds) let received = ctx.wait_for_email(&receiver.email, |e| { e.subject == "Integration Test Email" }, Duration::from_secs(5)).await; assert!(received.is_some()); assert!(received.unwrap().body.contains("Test content")); ctx.cleanup().await; } ``` ### 3. End-to-End Tests Full user journeys across multiple apps. ```rust // tests/e2e/scenarios/meeting_workflow_test.rs #[tokio::test] async fn test_complete_meeting_workflow() { let ctx = E2EContext::new().await; // 1. User A creates a meeting let host = ctx.login_as("host@test.gb.local").await; let meeting = host.create_meeting(MeetingConfig { title: "Sprint Planning", scheduled_at: Utc::now() + Duration::hours(1), participants: vec!["participant@test.gb.local"], }).await.unwrap(); // 2. Verify invitation email was sent let invite_email = ctx.wait_for_email( "participant@test.gb.local", |e| e.subject.contains("Sprint Planning"), Duration::from_secs(10) ).await.unwrap(); assert!(invite_email.body.contains("You've been invited")); assert!(invite_email.body.contains(&meeting.join_url)); // 3. Participant accepts invitation let participant = ctx.login_as("participant@test.gb.local").await; participant.accept_meeting_invite(&meeting.id).await.unwrap(); // 4. Verify calendar event was created for both let host_events = host.get_calendar_events(Utc::now(), Utc::now() + Duration::days(1)).await; let participant_events = participant.get_calendar_events(Utc::now(), Utc::now() + Duration::days(1)).await; assert!(host_events.iter().any(|e| e.title == "Sprint Planning")); assert!(participant_events.iter().any(|e| e.title == "Sprint Planning")); // 5. Start the meeting let room = host.start_meeting(&meeting.id).await.unwrap(); // 6. Participant joins participant.join_meeting(&meeting.id).await.unwrap(); // 7. Verify both are in the room let participants = ctx.get_meeting_participants(&meeting.id).await; assert_eq!(participants.len(), 2); // 8. Host ends meeting host.end_meeting(&meeting.id).await.unwrap(); // 9. Verify recording is available (if enabled) if meeting.recording_enabled { let recording = ctx.wait_for_recording(&meeting.id, Duration::from_secs(30)).await; assert!(recording.is_some()); } ctx.cleanup().await; } ``` --- ## Test Accounts Setup ### Account Types | Account | Email | Purpose | |---------|-------|---------| | Sender | sender@test.gb.local | Initiates actions | | Receiver | receiver@test.gb.local | Receives actions | | Bot | bot@test.gb.local | AI bot responses | | Admin | admin@test.gb.local | Admin operations | | External | external@example.com | External user simulation | ### Setup Script ```bash #!/bin/bash # scripts/setup_test_accounts.sh # Create test accounts in Stalwart stalwart-cli account create sender@test.gb.local --password test123 stalwart-cli account create receiver@test.gb.local --password test123 stalwart-cli account create bot@test.gb.local --password test123 stalwart-cli account create admin@test.gb.local --password test123 --admin # Create accounts in PostgreSQL psql $DATABASE_URL << EOF INSERT INTO test_accounts (account_type, email, password_hash, display_name) VALUES ('sender', 'sender@test.gb.local', '\$argon2...', 'Test Sender'), ('receiver', 'receiver@test.gb.local', '\$argon2...', 'Test Receiver'), ('bot', 'bot@test.gb.local', '\$argon2...', 'Test Bot'), ('admin', 'admin@test.gb.local', '\$argon2...', 'Test Admin') ON CONFLICT (email) DO NOTHING; EOF ``` ### Test Account Helper ```rust // tests/helpers/test_accounts.rs pub struct TestAccount { pub id: Uuid, pub email: String, pub password: String, pub account_type: AccountType, session: Option, } impl TestAccount { pub async fn create(ctx: &TestContext, account_type: AccountType) -> Self { let email = format!("{}_{:x}@test.gb.local", account_type.as_str(), rand::random::() ); // Create in Stalwart via API ctx.stalwart_client.create_account(&email, "test123").await.unwrap(); // Create in database let id = ctx.db.insert_test_account(&email, account_type).await.unwrap(); Self { id, email, password: "test123".into(), account_type, session: None, } } pub async fn login(&mut self, ctx: &TestContext) -> &Session { let session = ctx.auth_service.login(&self.email, &self.password).await.unwrap(); self.session = Some(session); self.session.as_ref().unwrap() } pub async fn cleanup(&self, ctx: &TestContext) { ctx.stalwart_client.delete_account(&self.email).await.ok(); ctx.db.delete_test_account(&self.id).await.ok(); } } ``` --- ## Email Testing ### Test Scenarios #### 1. Basic Send/Receive ```rust #[tokio::test] async fn test_email_basic_send_receive() { let ctx = TestContext::new().await; let sender = TestAccount::create(&ctx, AccountType::Sender).await; let receiver = TestAccount::create(&ctx, AccountType::Receiver).await; // Send email let sent = ctx.email.send(EmailRequest { from: sender.email.clone(), to: vec![receiver.email.clone()], subject: "Test Subject".into(), body_html: "

Test Body

".into(), body_plain: "Test Body".into(), }).await.unwrap(); // Verify sent assert!(sent.message_id.is_some()); // Wait for receive let received = ctx.wait_for_email(&receiver.email, |e| { e.subject == "Test Subject" }, Duration::from_secs(5)).await.unwrap(); assert_eq!(received.from, sender.email); assert!(received.body_html.contains("Test Body")); sender.cleanup(&ctx).await; receiver.cleanup(&ctx).await; } ``` #### 2. Global + User Signature ```rust #[tokio::test] async fn test_email_signatures() { let ctx = TestContext::new().await; let sender = TestAccount::create(&ctx, AccountType::Sender).await; let receiver = TestAccount::create(&ctx, AccountType::Receiver).await; // Set global signature for bot ctx.db.set_global_signature(ctx.bot_id, GlobalSignature { content_html: "

-- Powered by General Bots

".into(), content_plain: "-- Powered by General Bots".into(), position: SignaturePosition::Bottom, }).await.unwrap(); // Set user signature ctx.db.set_user_signature(&sender.id, UserSignature { content_html: "

Best regards,
John Doe

".into(), content_plain: "Best regards,\nJohn Doe".into(), is_default: true, }).await.unwrap(); // Send email ctx.email.send(EmailRequest { from: sender.email.clone(), to: vec![receiver.email.clone()], subject: "Signature Test".into(), body_html: "

Hello!

".into(), body_plain: "Hello!".into(), apply_signatures: true, }).await.unwrap(); // Verify signatures in received email let received = ctx.wait_for_email(&receiver.email, |e| { e.subject == "Signature Test" }, Duration::from_secs(5)).await.unwrap(); // Order: Body -> User Signature -> Global Signature let body = &received.body_html; let body_pos = body.find("Hello!").unwrap(); let user_sig_pos = body.find("John Doe").unwrap(); let global_sig_pos = body.find("General Bots").unwrap(); assert!(body_pos < user_sig_pos); assert!(user_sig_pos < global_sig_pos); sender.cleanup(&ctx).await; receiver.cleanup(&ctx).await; } ``` #### 3. Scheduled Send ```rust #[tokio::test] async fn test_scheduled_email() { let ctx = TestContext::new().await; let sender = TestAccount::create(&ctx, AccountType::Sender).await; let receiver = TestAccount::create(&ctx, AccountType::Receiver).await; let scheduled_time = Utc::now() + Duration::seconds(10); // Schedule email let scheduled = ctx.email.schedule(EmailRequest { from: sender.email.clone(), to: vec![receiver.email.clone()], subject: "Scheduled Test".into(), body_html: "

Scheduled content

".into(), scheduled_at: Some(scheduled_time), }).await.unwrap(); assert_eq!(scheduled.status, "pending"); // Verify NOT delivered yet tokio::time::sleep(Duration::from_secs(2)).await; let early_check = ctx.check_inbox(&receiver.email).await; assert!(!early_check.iter().any(|e| e.subject == "Scheduled Test")); // Wait for scheduled time + buffer tokio::time::sleep(Duration::from_secs(12)).await; // Verify delivered let received = ctx.check_inbox(&receiver.email).await; assert!(received.iter().any(|e| e.subject == "Scheduled Test")); sender.cleanup(&ctx).await; receiver.cleanup(&ctx).await; } ``` #### 4. Email Tracking ```rust #[tokio::test] async fn test_email_tracking() { let ctx = TestContext::new().await; let sender = TestAccount::create(&ctx, AccountType::Sender).await; let receiver = TestAccount::create(&ctx, AccountType::Receiver).await; // Send with tracking enabled let sent = ctx.email.send(EmailRequest { from: sender.email.clone(), to: vec![receiver.email.clone()], subject: "Tracked Email".into(), body_html: "

Track me

".into(), tracking_enabled: true, }).await.unwrap(); let tracking_id = sent.tracking_id.unwrap(); // Check initial status let status = ctx.email.get_tracking_status(&tracking_id).await.unwrap(); assert!(!status.is_read); assert_eq!(status.read_count, 0); // Simulate email open (load tracking pixel) ctx.http_client.get(&format!( "{}/api/email/track/{}.gif", ctx.server_url, tracking_id )).send().await.unwrap(); // Check updated status let status = ctx.email.get_tracking_status(&tracking_id).await.unwrap(); assert!(status.is_read); assert_eq!(status.read_count, 1); assert!(status.read_at.is_some()); sender.cleanup(&ctx).await; receiver.cleanup(&ctx).await; } ``` #### 5. Auto-Responder (Out of Office) ```rust #[tokio::test] async fn test_auto_responder() { let ctx = TestContext::new().await; let sender = TestAccount::create(&ctx, AccountType::Sender).await; let receiver = TestAccount::create(&ctx, AccountType::Receiver).await; // Set up auto-responder for receiver ctx.email.set_auto_responder(&receiver.id, AutoResponder { subject: "Out of Office".into(), body_html: "

I'm currently away. Will respond when I return.

".into(), start_date: Utc::now() - Duration::hours(1), end_date: Utc::now() + Duration::days(7), is_active: true, }).await.unwrap(); // Sync to Stalwart Sieve ctx.stalwart.sync_sieve_rules(&receiver.email).await.unwrap(); // Send email to receiver ctx.email.send(EmailRequest { from: sender.email.clone(), to: vec![receiver.email.clone()], subject: "Question".into(), body_html: "

Can we meet tomorrow?

".into(), }).await.unwrap(); // Wait for auto-response let auto_reply = ctx.wait_for_email(&sender.email, |e| { e.subject.contains("Out of Office") }, Duration::from_secs(10)).await; assert!(auto_reply.is_some()); assert!(auto_reply.unwrap().body.contains("currently away")); sender.cleanup(&ctx).await; receiver.cleanup(&ctx).await; } ``` --- ## Calendar & Meeting Testing ### Test Scenarios #### 1. Meeting Invitation Flow ```rust #[tokio::test] async fn test_meeting_invitation_accept_decline() { let ctx = TestContext::new().await; let host = TestAccount::create(&ctx, AccountType::Sender).await; let participant1 = TestAccount::create(&ctx, AccountType::Receiver).await; let participant2 = TestAccount::create(&ctx, AccountType::Receiver).await; // Host creates meeting let meeting = ctx.calendar.create_event(CalendarEvent { organizer: host.email.clone(), title: "Team Standup".into(), start_time: Utc::now() + Duration::hours(2), end_time: Utc::now() + Duration::hours(3), participants: vec![ participant1.email.clone(), participant2.email.clone(), ], is_meeting: true, }).await.unwrap(); // Wait for invitation emails let invite1 = ctx.wait_for_email(&participant1.email, |e| { e.subject.contains("Team Standup") && e.content_type.contains("text/calendar") }, Duration::from_secs(10)).await.unwrap(); let invite2 = ctx.wait_for_email(&participant2.email, |e| { e.subject.contains("Team Standup") }, Duration::from_secs(10)).await.unwrap(); // Participant 1 accepts ctx.calendar.respond_to_invite(&participant1.id, &meeting.id, Response::Accept).await.unwrap(); // Participant 2 declines ctx.calendar.respond_to_invite(&participant2.id, &meeting.id, Response::Decline).await.unwrap(); // Host receives response notifications let accept_notification = ctx.wait_for_email(&host.email, |e| { e.subject.contains("Accepted") && e.subject.contains("Team Standup") }, Duration::from_secs(10)).await; let decline_notification = ctx.wait_for_email(&host.email, |e| { e.subject.contains("Declined") && e.subject.contains("Team Standup") }, Duration::from_secs(10)).await; assert!(accept_notification.is_some()); assert!(decline_notification.is_some()); // Verify meeting participants let updated_meeting = ctx.calendar.get_event(&meeting.id).await.unwrap(); assert_eq!(updated_meeting.participant_status(&participant1.email), Some(ParticipantStatus::Accepted)); assert_eq!(updated_meeting.participant_status(&participant2.email), Some(ParticipantStatus::Declined)); host.cleanup(&ctx).await; participant1.cleanup(&ctx).await; participant2.cleanup(&ctx).await; } ``` #### 2. Video Meeting Lifecycle ```rust #[tokio::test] async fn test_video_meeting_full_lifecycle() { let ctx = TestContext::new().await; let host = TestAccount::create(&ctx, AccountType::Sender).await; let participant = TestAccount::create(&ctx, AccountType::Receiver).await; // 1. Create meeting room let room = ctx.meet.create_room(MeetingRoom { name: "Test Meeting Room".into(), host_id: host.id, settings: RoomSettings { enable_waiting_room: true, enable_recording: true, max_participants: 10, }, }).await.unwrap(); // 2. Host joins let host_token = ctx.meet.generate_token(&room.id, &host.id, TokenRole::Host).await.unwrap(); let host_connection = ctx.livekit.connect(&room.name, &host_token).await.unwrap(); assert!(host_connection.is_connected()); // 3. Participant tries to join (goes to waiting room) let participant_token = ctx.meet.generate_token(&room.id, &participant.id, TokenRole::Participant).await.unwrap(); let waiting_entry = ctx.meet.request_join(&room.id, &participant.id).await.unwrap(); assert_eq!(waiting_entry.status, WaitingStatus::Waiting); // 4. Host admits participant ctx.meet.admit_participant(&room.id, &participant.id, &host.id).await.unwrap(); // 5. Participant joins let participant_connection = ctx.livekit.connect(&room.name, &participant_token).await.unwrap(); assert!(participant_connection.is_connected()); // 6. Verify both in room let participants = ctx.meet.get_participants(&room.id).await.unwrap(); assert_eq!(participants.len(), 2); // 7. Start recording ctx.meet.start_recording(&room.id, &host.id).await.unwrap(); tokio::time::sleep(Duration::from_secs(5)).await; // 8. End meeting ctx.meet.end_meeting(&room.id, &host.id).await.unwrap(); // 9. Verify recording exists let recording = ctx.wait_for_condition(|| async { ctx.meet.get_recording(&room.id).await.ok() }, Duration::from_secs(30)).await.unwrap(); assert!(recording.file_size > 0); assert!(recording.duration_seconds.unwrap() >= 5); host.cleanup(&ctx).await; participant.cleanup(&ctx).await; } ``` #### 3. Meeting Breakout Rooms ```rust #[tokio::test] async fn test_breakout_rooms() { let ctx = TestContext::new().await; let host = TestAccount::create(&ctx, AccountType::Sender).await; let participants: Vec<_> = (0..6).map(|_| { TestAccount::create(&ctx, AccountType::Receiver) }).collect::>().collect().await; // Create main meeting let meeting = ctx.meet.create_room(MeetingRoom { name: "Workshop".into(), host_id: host.id, settings: Default::default(), }).await.unwrap(); // Everyone joins for p in &participants { ctx.meet.join(&meeting.id, &p.id).await.unwrap(); } // Create breakout rooms let breakout1 = ctx.meet.create_breakout_room(&meeting.id, "Group A").await.unwrap(); let breakout2 = ctx.meet.create_breakout_room(&meeting.id, "Group B").await.unwrap(); // Assign participants (3 each) for (i, p) in participants.iter().enumerate() { let room = if i < 3 { &breakout1.id } else { &breakout2.id }; ctx.meet.assign_to_breakout(room, &p.id).await.unwrap(); } // Start breakout sessions ctx.meet.start_breakout_rooms(&meeting.id).await.unwrap(); // Verify participants are in correct rooms let room1_participants = ctx.meet.get_breakout_participants(&breakout1.id).await.unwrap(); let room2_participants = ctx.meet.get_breakout_participants(&breakout2.id).await.unwrap(); assert_eq!(room1_participants.len(), 3); assert_eq!(room2_participants.len(), 3); // Close breakout rooms ctx.meet.close_breakout_rooms(&meeting.id).await.unwrap(); // Verify everyone back in main room let main_participants = ctx.meet.get_participants(&meeting.id).await.unwrap(); assert_eq!(main_participants.len(), 7); // 6 participants + 1 host host.cleanup(&ctx).await; for p in participants { p.cleanup(&ctx).await; } } ``` --- ## Drive Testing ### Test Scenarios #### 1. File Upload and Share ```rust #[tokio::test] async fn test_file_upload_and_share() { let ctx = TestContext::new().await; let owner = TestAccount::create(&ctx, AccountType::Sender).await; let collaborator = TestAccount::create(&ctx, AccountType::Receiver).await; // Upload file let file_content = b"Test document content"; let uploaded = ctx.drive.upload(UploadRequest { user_id: owner.id, filename: "test.txt".into(), content: file_content.to_vec(), content_type: "text/plain".into(), }).await.unwrap(); assert!(uploaded.file_id.is_some()); assert_eq!(uploaded.size, file_content.len() as i64); // Share with collaborator let share = ctx.drive.share(ShareRequest { file_id: uploaded.file_id.unwrap(), shared_by: owner.id, shared_with_user: Some(collaborator.id), permission: Permission::Edit, }).await.unwrap(); assert!(share.link_token.is_some()); // Verify collaborator can access let files = ctx.drive.list_shared_with_me(&collaborator.id).await.unwrap(); assert!(files.iter().any(|f| f.filename == "test.txt")); // Verify collaborator can edit let edit_result = ctx.drive.update_content( &uploaded.file_id.unwrap(), &collaborator.id, b"Modified content".to_vec() ).await; assert!(edit_result.is_ok()); owner.cleanup(&ctx).await; collaborator.cleanup(&ctx).await; } ``` #### 2. Version History ```rust #[tokio::test] async fn test_file_version_history() { let ctx = TestContext::new().await; let user = TestAccount::create(&ctx, AccountType::Sender).await; // Upload initial version let uploaded = ctx.drive.upload(UploadRequest { user_id: user.id, filename: "document.txt".into(), content: b"Version 1".to_vec(), content_type: "text/plain".into(), }).await.unwrap(); let file_id = uploaded.file_id.unwrap(); // Update file multiple times for i in 2..=5 { ctx.drive.update_content( &file_id, &user.id, format!("Version {}", i).into_bytes() ).await.unwrap(); } // Get version history let versions = ctx.drive.get_versions(&file_id).await.unwrap(); assert_eq!(versions.len(), 5); assert_eq!(versions[0].version_number, 1); assert_eq!(versions[4].version_number, 5); // Restore to version 2 ctx.drive.restore_version(&file_id, 2, &user.id).await.unwrap(); // Verify content let content = ctx.drive.download(&file_id).await.unwrap(); assert_eq!(String::from_utf8(content.data).unwrap(), "Version 2"); // New version should be 6 let versions = ctx.drive.get_versions(&file_id).await.unwrap(); assert_eq!(versions.len(), 6); user.cleanup(&ctx).await; } ``` #### 3. Offline Sync ```rust #[tokio::test] async fn test_offline_sync_conflict() { let ctx = TestContext::new().await; let user = TestAccount::create(&ctx, AccountType::Sender).await; let device1 = "device_desktop"; let device2 = "device_laptop"; // Upload file let uploaded = ctx.drive.upload(UploadRequest { user_id: user.id, filename: "shared.txt".into(), content: b"Original".to_vec(), content_type: "text/plain".into(), }).await.unwrap(); let file_id = uploaded.file_id.unwrap(); // Mark as synced on both devices ctx.drive.mark_synced(&file_id, &user.id, device1, 1).await.unwrap(); ctx.drive.mark_synced(&file_id, &user.id, device2, 1).await.unwrap(); // Simulate offline edits on both devices ctx.drive.report_local_change(&file_id, &user.id, device1, b"Edit from desktop".to_vec()).await.unwrap(); ctx.drive.report_local_change(&file_id, &user.id, device2, b"Edit from laptop".to_vec()).await.unwrap(); // Sync device1 first let sync1 = ctx.drive.sync(&file_id, &user.id, device1).await.unwrap(); assert_eq!(sync1.status, SyncStatus::Synced); // Sync device2 - should detect conflict let sync2 = ctx.drive.sync(&file_id, &user.id, device2).await.unwrap(); assert_eq!(sync2.status, SyncStatus::Conflict); assert!(sync2.conflict_data.is_some()); // Resolve conflict ctx.drive.resolve_conflict(&file_id, &user.id, device2, ConflictResolution::KeepBoth).await.unwrap(); // Verify both versions exist let files = ctx.drive.list(&user.id, "/").await.unwrap(); assert!(files.iter().any(|f| f.filename == "shared.txt")); assert!(files.iter().any(|f| f.filename.contains("conflict"))); user.cleanup(&ctx).await; } ``` --- ## Bot Response Testing ### Test Scenarios #### 1. Bot Responds to Email Content ```rust #[tokio::test] async fn test_bot_email_response() { let ctx = TestContext::new().await; let user = TestAccount::create(&ctx, AccountType::Sender).await; let bot = ctx.get_test_bot().await; // Send email to bot ctx.email.send(EmailRequest { from: user.email.clone(), to: vec![bot.email.clone()], subject: "Question about pricing".into(), body_html: "

What are your enterprise pricing options?

".into(), }).await.unwrap(); // Wait for bot response let response = ctx.wait_for_email(&user.email, |e| { e.from == bot.email && e.subject.contains("Re: Question about pricing") }, Duration::from_secs(30)).await; assert!(response.is_some()); let response = response.unwrap(); // Verify response quality assert!(response.body.to_lowercase().contains("pricing") || response.body.to_lowercase().contains("enterprise") || response.body.to_lowercase().contains("plan")); // Verify response uses KB content let kb_keywords = ["contact sales", "custom quote", "enterprise tier"]; assert!(kb_keywords.iter().any(|kw| response.body.to_lowercase().contains(kw))); user.cleanup(&ctx).await; } ``` #### 2. Bot with KB Context ```rust #[tokio::test] async fn test_bot_kb_integration() { let ctx = TestContext::new().await; let user = TestAccount::create(&ctx, AccountType::Sender).await; let bot = ctx.get_test_bot().await; // Add document to KB ctx.kb.add_document(&bot.id, Document { title: "Product FAQ".into(), content: "Q: What is the return policy? A: 30-day money-back guarantee.".into(), collection: "faq".into(), }).await.unwrap(); // Send question that should match KB ctx.email.send(EmailRequest { from: user.email.clone(), to: vec![bot.email.clone()], subject: "Return policy question".into(), body_html: "

Can I return my purchase?

".into(), }).await.unwrap(); // Wait for response let response = ctx.wait_for_email(&user.email, |e| { e.from == bot.email }, Duration::from_secs(30)).await.unwrap(); // Should contain KB information assert!(response.body.contains("30-day") || response.body.contains("money-back")); user.cleanup(&ctx).await; } ``` #### 3. Bot Multi-turn Conversation ```rust #[tokio::test] async fn test_bot_conversation_context() { let ctx = TestContext::new().await; let user = TestAccount::create(&ctx, AccountType::Sender).await; let bot = ctx.get_test_bot().await; // First message ctx.chat.send(&user.id, &bot.id, "My name is John").await.unwrap(); // Wait for acknowledgment ctx.wait_for_chat_response(&user.id, &bot.id, Duration::from_secs(10)).await.unwrap(); // Second message - should remember name ctx.chat.send(&user.id, &bot.id, "What is my name?").await.unwrap(); // Wait for response let response = ctx.wait_for_chat_response(&user.id, &bot.id, Duration::from_secs(10)).await.unwrap(); // Should remember the name assert!(response.content.to_lowercase().contains("john")); user.cleanup(&ctx).await; } ``` --- ## Integration Testing ### Multi-Service Workflows #### 1. Email → Calendar → Meet ```rust #[tokio::test] async fn test_email_to_meeting_workflow() { let ctx = TestContext::new().await; let organizer = TestAccount::create(&ctx, AccountType::Sender).await; let attendee = TestAccount::create(&ctx, AccountType::Receiver).await; // 1. Organizer sends meeting request via email with .ics let meeting_time = Utc::now() + Duration::hours(24); let ics = generate_ics_invite(IcsConfig { organizer: &organizer.email, attendee: &attendee.email, title: "Project Review", start: meeting_time, duration: Duration::hours(1), }); ctx.email.send(EmailRequest { from: organizer.email.clone(), to: vec![attendee.email.clone()], subject: "Meeting: Project Review".into(), body_html: "

Please join our project review meeting.

".into(), attachments: vec![Attachment { filename: "invite.ics".into(), content_type: "text/calendar".into(), data: ics.into_bytes(), }], }).await.unwrap(); // 2. Attendee receives and accepts let invite = ctx.wait_for_email(&attendee.email, |e| { e.subject.contains("Project Review") }, Duration::from_secs(10)).await.unwrap(); // Process ICS attachment ctx.calendar.process_ics_invite(&attendee.id, &invite.attachments[0].data).await.unwrap(); // 3. Verify calendar event created let events = ctx.calendar.get_events(&attendee.id, Utc::now(), Utc::now() + Duration::days(2) ).await.unwrap(); assert!(events.iter().any(|e| e.title == "Project Review")); // 4. At meeting time, verify meeting room is available let event = events.iter().find(|e| e.title == "Project Review").unwrap(); let meeting_room = ctx.meet.get_room_for_event(&event.id).await.unwrap(); assert!(meeting_room.is_some()); organizer.cleanup(&ctx).await; attendee.cleanup(&ctx).await; } ``` #### 2. Chat → Drive → Email ```rust #[tokio::test] async fn test_chat_file_share_workflow() { let ctx = TestContext::new().await; let sender = TestAccount::create(&ctx, AccountType::Sender).await; let receiver = TestAccount::create(&ctx, AccountType::Receiver).await; // 1. User uploads file via chat let upload_message = ctx.chat.send_with_attachment( &sender.id, &receiver.id, "Here's the report", Attachment { filename: "report.pdf".into(), content_type: "application/pdf".into(), data: include_bytes!("../fixtures/documents/sample.pdf").to_vec(), } ).await.unwrap(); // 2. File should be stored in Drive let drive_files = ctx.drive.list(&sender.id, "/").await.unwrap(); assert!(drive_files.iter().any(|f| f.filename == "report.pdf")); // 3. Receiver gets notification via email let notification = ctx.wait_for_email(&receiver.email, |e| { e.subject.contains("shared a file") || e.subject.contains("report.pdf") }, Duration::from_secs(10)).await; assert!(notification.is_some()); // 4. Receiver can access file let shared_files = ctx.drive.list_shared_with_me(&receiver.id).await.unwrap(); assert!(shared_files.iter().any(|f| f.filename == "report.pdf")); sender.cleanup(&ctx).await; receiver.cleanup(&ctx).await; } ``` --- ## Load & Performance Testing ### Configuration ```rust // tests/load/config.rs pub struct LoadTestConfig { pub concurrent_users: usize, pub duration: Duration, pub ramp_up: Duration, pub target_rps: f64, } impl Default for LoadTestConfig { fn default() -> Self { Self { concurrent_users: 100, duration: Duration::from_secs(300), ramp_up: Duration::from_secs(60), target_rps: 1000.0, } } } ``` ### Scenarios ```rust // tests/load/email_load_test.rs #[tokio::test] #[ignore] // Run manually: cargo test load -- --ignored async fn test_email_sending_load() { let config = LoadTestConfig { concurrent_users: 50, duration: Duration::from_secs(60), ..Default::default() }; let results = run_load_test(config, |ctx, user_id| async move { let start = Instant::now(); ctx.email.send(EmailRequest { from: format!("user{}@test.local", user_id), to: vec!["receiver@test.local".into()], subject: format!("Load test {}", Uuid::new_v4()), body_html: "

Test

".into(), }).await?; Ok(start.elapsed()) }).await; // Assertions assert!(results.success_rate > 0.99); // 99%+ success assert!(results.p95_latency < Duration::from_millis(500)); assert!(results.p99_latency < Duration::from_secs(1)); println!("Load Test Results:"); println!(" Total requests: {}", results.total_requests); println!(" Success rate: {:.2}%", results.success_rate * 100.0); println!(" Avg latency: {:?}", results.avg_latency); println!(" P95 latency: {:?}", results.p95_latency); println!(" P99 latency: {:?}", results.p99_latency); println!(" Throughput: {:.2} req/s", results.throughput); } ``` --- ## CI/CD Pipeline ### GitHub Actions Workflow ```yaml # .github/workflows/test.yml name: Test Suite on: push: branches: [main, develop] pull_request: branches: [main] env: CARGO_TERM_COLOR: always DATABASE_URL: postgresql://test:test@localhost:5432/gb_test jobs: unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Cache cargo uses: actions/cache@v3 with: path: | ~/.cargo/registry ~/.cargo/git target key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - name: Run unit tests run: cargo test --lib -- --test-threads=4 working-directory: botserver integration-tests: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_USER: test POSTGRES_PASSWORD: test POSTGRES_DB: gb_test ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 stalwart: image: stalwartlabs/mail-server:latest ports: - 8080:8080 - 25:25 - 143:143 minio: image: minio/minio:latest ports: - 9000:9000 env: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin steps: - uses: actions/checkout@v4 - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Setup test environment run: | ./scripts/setup_test_accounts.sh ./scripts/run_migrations.sh - name: Run integration tests run: cargo test --test '*' -- --test-threads=1 working-directory: botserver env: TEST_STALWART_URL: http://localhost:8080 TEST_MINIO_ENDPOINT: http://localhost:9000 e2e-tests: runs-on: ubuntu-latest needs: [unit-tests, integration-tests] steps: - uses: actions/checkout@v4 - name: Start full environment run: docker-compose -f docker-compose.test.yml up -d - name: Wait for services run: ./scripts/wait_for_services.sh - name: Run E2E tests run: cargo test --test e2e -- --test-threads=1 working-directory: botserver - name: Collect logs on failure if: failure() run: docker-compose -f docker-compose.test.yml logs > test-logs.txt - name: Upload logs if: failure() uses: actions/upload-artifact@v3 with: name: test-logs path: test-logs.txt ``` --- ## Test Data Management ### Fixtures ``` tests/fixtures/ ├── emails/ │ ├── simple.eml │ ├── with_attachments.eml │ ├── calendar_invite.eml │ └── html_rich.eml ├── documents/ │ ├── sample.pdf │ ├── spreadsheet.xlsx │ └── presentation.pptx ├── responses/ │ ├── pricing_question.json │ ├── support_request.json │ └── general_inquiry.json └── calendar/ ├── simple_event.ics ├── recurring_event.ics └── meeting_invite.ics ``` ### Cleanup Strategy ```rust // tests/helpers/cleanup.rs pub struct TestContext { created_accounts: Vec, created_files: Vec, created_events: Vec, created_emails: Vec, } impl TestContext { pub async fn cleanup(&self) { // Cleanup in reverse order of dependencies // 1. Delete emails for message_id in &self.created_emails { self.stalwart.delete_message(message_id).await.ok(); } // 2. Delete events for event_id in &self.created_events { self.calendar.delete_event(event_id).await.ok(); } // 3. Delete files for file_id in &self.created_files { self.drive.permanent_delete(file_id).await.ok(); } // 4. Delete accounts for account_id in &self.created_accounts { self.db.delete_test_account(account_id).await.ok(); self.stalwart.delete_account_by_id(account_id).await.ok(); } } } impl Drop for TestContext { fn drop(&mut self) { // Ensure cleanup runs even on panic if !self.created_accounts.is_empty() { eprintln!("WARNING: Test context dropped without cleanup!"); // Log for manual cleanup } } } ``` ### Database Seeding ```sql -- tests/fixtures/seed.sql -- Test bot configuration INSERT INTO bots (id, name, description, llm_provider, llm_config, context_provider, context_config) VALUES ( '00000000-0000-0000-0000-000000000001', 'Test Bot', 'Bot for automated testing', 'openai', '{"model": "gpt-5", "temperature": 0.7}', 'qdrant', '{"collection": "test_kb"}' ); -- Global signature INSERT INTO global_email_signatures (bot_id, name, content_html, content_plain, is_active) VALUES ( '00000000-0000-0000-0000-000000000001', 'Default', '

--
Powered by General Bots
www.generalbots.com

', '-- \nPowered by General Bots\nwww.generalbots.com', true ); ``` --- ## Summary ### Test Coverage Targets | Category | Target Coverage | Priority | |----------|----------------|----------| | Email Send/Receive | 95% | P0 | | Email Signatures | 90% | P1 | | Email Scheduling | 90% | P1 | | Calendar Events | 90% | P0 | | Meeting Invites | 95% | P0 | | Video Meetings | 85% | P1 | | File Upload/Download | 95% | P0 | | File Sharing | 90% | P1 | | Bot Responses | 85% | P0 | | Multi-service Flows | 80% | P1 | ### Running Tests ```bash # All unit tests cargo test --lib # Integration tests (requires services) cargo test --test integration # E2E tests (requires full stack) cargo test --test e2e # Specific test cargo test test_email_signatures # With logging RUST_LOG=debug cargo test test_name -- --nocapture # Load tests (manual) cargo test load -- --ignored --nocapture ``` ### Monitoring Test Health - Track flaky tests in CI - Monitor test execution time trends - Review coverage reports weekly - Update fixtures when APIs change