# 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
John Doe
CEO
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: OptionTest 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
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::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--
Powered by General Bots
www.generalbots.com