botserver/docs/TESTING_STRATEGY.md

1447 lines
No EOL
44 KiB
Markdown

# 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 = "<p>-- <br>General Bots Team</p>";
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 = "<p><b>John Doe</b><br>CEO</p>";
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("<p>Test content</p>")
.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<Session>,
}
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::<u32>()
);
// 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: "<p>Test Body</p>".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: "<p>-- Powered by General Bots</p>".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: "<p>Best regards,<br>John Doe</p>".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: "<p>Hello!</p>".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: "<p>Scheduled content</p>".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: "<p>Track me</p>".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: "<p>I'm currently away. Will respond when I return.</p>".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: "<p>Can we meet tomorrow?</p>".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::<FuturesUnordered<_>>().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: "<p>What are your enterprise pricing options?</p>".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: "<p>Can I return my purchase?</p>".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: "<p>Please join our project review meeting.</p>".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: "<p>Test</p>".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<Uuid>,
created_files: Vec<Uuid>,
created_events: Vec<Uuid>,
created_emails: Vec<String>,
}
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',
'<p>-- <br>Powered by General Bots<br>www.generalbots.com</p>',
'-- \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