diff --git a/tests/email_integration_test.rs b/tests/email_integration_test.rs new file mode 100644 index 0000000..9793f76 --- /dev/null +++ b/tests/email_integration_test.rs @@ -0,0 +1,102 @@ +// Integration tests for Email-CRM-Campaigns features +use serde_json::json; + +#[tokio::test] +async fn test_feature_flags_endpoint() { + let client = reqwest::Client::new(); + let org_id = "00000000-0000-0000-0000-000000000000"; + + let response = client + .get(&format!("http://localhost:8080/api/features/{}/enabled", org_id)) + .send() + .await; + + if let Ok(resp) = response { + assert!(resp.status().is_success() || resp.status().is_client_error()); + } +} + +#[tokio::test] +async fn test_extract_lead_endpoint() { + let client = reqwest::Client::new(); + + let payload = json!({ + "from": "john.doe@example.com", + "subject": "Interested in your product", + "body": "I would like to know more about pricing" + }); + + let response = client + .post("http://localhost:8080/api/ai/extract-lead") + .json(&payload) + .send() + .await; + + if let Ok(resp) = response { + assert!(resp.status().is_success() || resp.status().is_client_error()); + } +} + +#[tokio::test] +async fn test_categorize_email_endpoint() { + let client = reqwest::Client::new(); + + let payload = json!({ + "from": "customer@example.com", + "subject": "Need help with my account", + "body": "I'm having trouble logging in" + }); + + let response = client + .post("http://localhost:8080/api/ai/categorize-email") + .json(&payload) + .send() + .await; + + if let Ok(resp) = response { + if resp.status().is_success() { + let data: serde_json::Value = resp.json().await.unwrap(); + assert!(data.get("category").is_some()); + } + } +} + +#[tokio::test] +async fn test_snooze_email_endpoint() { + let client = reqwest::Client::new(); + + let payload = json!({ + "email_ids": ["00000000-0000-0000-0000-000000000001"], + "preset": "tomorrow" + }); + + let response = client + .post("http://localhost:8080/api/email/snooze") + .json(&payload) + .send() + .await; + + if let Ok(resp) = response { + assert!(resp.status().is_success() || resp.status().is_client_error()); + } +} + +#[tokio::test] +async fn test_flag_email_endpoint() { + let client = reqwest::Client::new(); + + let payload = json!({ + "email_ids": ["00000000-0000-0000-0000-000000000001"], + "follow_up": "today" + }); + + let response = client + .post("http://localhost:8080/api/email/flag") + .json(&payload) + .send() + .await; + + if let Ok(resp) = response { + assert!(resp.status().is_success() || resp.status().is_client_error()); + } +} diff --git a/tests/integration/accessibility.rs b/tests/integration/accessibility.rs new file mode 100644 index 0000000..d07c969 --- /dev/null +++ b/tests/integration/accessibility.rs @@ -0,0 +1,316 @@ +mod accessibility; + +use bottest::prelude::*; +use reqwest::Client; +use serde_json::json; +use std::time::Duration; + +fn test_client() -> Client { + Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .expect("Failed to create HTTP client") +} + +fn external_server_url() -> Option { + std::env::var("BOTSERVER_URL").ok() +} + +async fn get_test_server() -> Option<(Option, String)> { + if let Some(url) = external_server_url() { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(2)) + .build() + .ok()?; + + if client.get(&url).send().await.is_ok() { + return Some((None, url)); + } + } + + let ctx = TestHarness::quick().await.ok()?; + let server = ctx.start_botserver().await.ok()?; + + if server.is_running() { + Some((Some(ctx), server.url.clone())) + } else { + None + } +} + +macro_rules! skip_if_no_server { + ($base_url:expr) => { + if $base_url.is_none() { + eprintln!("Skipping test: no server available"); + return; + } + }; +} + +#[tokio::test] +async fn test_page_has_title() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/suite/auth/login.html", base_url)) + .send() + .await; + + match response { + Ok(resp) => { + if resp.status().is_success() { + let body = resp.text().await.unwrap_or_default(); + assert!( + body.contains("") || body.contains("<Title>"), + "Page should have a title element" + ); + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_form_labels_present() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/suite/auth/login.html", base_url)) + .send() + .await; + + match response { + Ok(resp) => { + if resp.status().is_success() { + let body = resp.text().await.unwrap_or_default(); + + let has_label = body.contains("<label") || body.contains("aria-label"); + let has_input = body.contains("<input"); + + if has_input { + assert!( + has_label, + "Form inputs should have associated labels" + ); + } + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_button_has_text() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/suite/auth/login.html", base_url)) + .send() + .await; + + match response { + Ok(resp) => { + if resp.status().is_success() { + let body = resp.text().await.unwrap_or_default(); + + let has_buttons = body.contains("<button"); + + if has_buttons { + let has_button_text = body.contains("</button>"); + assert!( + has_button_text, + "Buttons should have accessible text" + ); + } + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_images_have_alt_text() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/suite/auth/login.html", base_url)) + .send() + .await; + + match response { + Ok(resp) => { + if resp.status().is_success() { + let body = resp.text().await.unwrap_or_default(); + + let has_images = body.contains("<img"); + + if has_images { + let has_alt = body.contains("alt="); + assert!( + has_alt, + "Images should have alt attribute for accessibility" + ); + } + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_headings_have_proper_order() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/suite/auth/login.html", base_url)) + .send() + .await; + + match response { + Ok(resp) => { + if resp.status().is_success() { + let body = resp.text().await.unwrap_or_default(); + + let h1_count = body.matches("<h1").count(); + let h2_count = body.matches("<h2").count(); + + if h1_count > 1 { + eprintln!("Note: Multiple h1 elements found - consider using single h1"); + } + + if h2_count > 0 && h1_count == 0 { + eprintln!("Note: Page has h2 but no h1 - heading structure may be suboptimal"); + } + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_color_contrast() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/suite/auth/login.html", base_url)) + .send() + .await; + + match response { + Ok(resp) => { + if resp.status().is_success() { + let body = resp.text().await.unwrap_or_default(); + + assert!( + body.contains("color:") || body.contains("background") || body.contains("class="), + "Page should have color styling for visual accessibility" + ); + + eprintln!("Note: Color contrast testing requires visual analysis or automated tools"); + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_keyboard_navigation() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/suite/auth/login.html", base_url)) + .send() + .await; + + match response { + Ok(resp) => { + if resp.status().is_success() { + let body = resp.text().await.unwrap_or_default(); + + let has_focusable = body.contains("tabindex=") || body.contains("href="); + + assert!( + has_focusable, + "Page should have keyboard-navigable elements" + ); + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_error_messages_accessible() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .post(format!("{}/api/auth/login", base_url)) + .json(&json!({ + "username": "", + "password": "" + })) + .send() + .await; + + match response { + Ok(resp) => { + if resp.status() == 400 || resp.status() == 422 { + let body = resp.text().await.unwrap_or_default(); + + assert!( + body.len() > 0, + "Error responses should provide accessible error messages" + ); + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} diff --git a/tests/integration/compliance.rs b/tests/integration/compliance.rs new file mode 100644 index 0000000..f23eca3 --- /dev/null +++ b/tests/integration/compliance.rs @@ -0,0 +1,357 @@ +mod compliance; + +use bottest::prelude::*; +use reqwest::Client; +use serde_json::json; +use std::time::Duration; + +fn test_client() -> Client { + Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .expect("Failed to create HTTP client") +} + +fn external_server_url() -> Option<String> { + std::env::var("BOTSERVER_URL").ok() +} + +async fn get_test_server() -> Option<(Option<TestContext>, String)> { + if let Some(url) = external_server_url() { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(2)) + .build() + .ok()?; + + if client.get(&url).send().await.is_ok() { + return Some((None, url)); + } + } + + let ctx = TestHarness::quick().await.ok()?; + let server = ctx.start_botserver().await.ok()?; + + if server.is_running() { + Some((Some(ctx), server.url.clone())) + } else { + None + } +} + +macro_rules! skip_if_no_server { + ($base_url:expr) => { + if $base_url.is_none() { + eprintln!("Skipping test: no server available"); + return; + } + }; +} + +#[tokio::test] +async fn test_audit_log_for_sensitive_operations() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let login_response = client + .post(format!("{}/api/auth/login", base_url)) + .json(&json!({ + "username": "admin", + "password": "admin" + })) + .send() + .await; + + if let Ok(resp) = login_response { + if resp.status() == 200 { + let body: serde_json::Value = resp.json().await.unwrap_or_default(); + if let Some(token) = body.get("token").and_then(|t| t.as_str()) { + let create_response = client + .post(format!("{}/api/bots", base_url)) + .header("Authorization", format!("Bearer {}", token)) + .json(&json!({ + "name": "test_bot_audit" + })) + .send() + .await; + + if let Ok(resp) = create_response { + assert!( + resp.status() == 200 || resp.status() == 201 || resp.status() == 400, + "Bot creation should be logged or validated" + ); + } + } + } + } +} + +#[tokio::test] +async fn test_gdpr_data_deletion_endpoint() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let login_response = client + .post(format!("{}/api/auth/login", base_url)) + .json(&json!({ + "username": "testuser", + "password": "testpass" + })) + .send() + .await; + + if let Ok(resp) = login_response { + if resp.status() == 200 { + let body: serde_json::Value = resp.json().await.unwrap_or_default(); + if let Some(token) = body.get("token").and_then(|t| t.as_str()) { + let delete_response = client + .delete(format!("{}/api/users/me", base_url)) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await; + + if let Ok(resp) = delete_response { + assert!( + resp.status() == 200 || resp.status() == 204 || resp.status() == 404 || resp.status() == 401, + "GDPR deletion endpoint should exist and respond" + ); + } + } + } + } +} + +#[tokio::test] +async fn test_gdpr_data_export_endpoint() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let login_response = client + .post(format!("{}/api/auth/login", base_url)) + .json(&json!({ + "username": "testuser", + "password": "testpass" + })) + .send() + .await; + + if let Ok(resp) = login_response { + if resp.status() == 200 { + let body: serde_json::Value = resp.json().await.unwrap_or_default(); + if let Some(token) = body.get("token").and_then(|t| t.as_str()) { + let export_response = client + .get(format!("{}/api/users/me/export", base_url)) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await; + + if let Ok(resp) = export_response { + if resp.status() == 200 { + let _body: serde_json::Value = resp.json().await.unwrap_or_default(); + } + } + } + } + } +} + +#[tokio::test] +async fn test_consent_tracking_endpoint() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let login_response = client + .post(format!("{}/api/auth/login", base_url)) + .json(&json!({ + "username": "testuser", + "password": "testpass" + })) + .send() + .await; + + if let Ok(resp) = login_response { + if resp.status() == 200 { + let body: serde_json::Value = resp.json().await.unwrap_or_default(); + if let Some(token) = body.get("token").and_then(|t| t.as_str()) { + let consent = json!({ + "marketing": true, + "analytics": false, + "timestamp": "2024-01-01T00:00:00Z" + }); + + let response = client + .post(format!("{}/api/users/me/consent", base_url)) + .header("Authorization", format!("Bearer {}", token)) + .json(&consent) + .send() + .await; + + if let Ok(resp) = response { + assert!( + resp.status() == 200 || resp.status() == 201 || resp.status() == 404, + "Consent endpoint should exist" + ); + } + } + } + } +} + +#[tokio::test] +async fn test_session_isolation() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client1 = test_client(); + let client2 = test_client(); + + let login1 = client1 + .post(format!("{}/api/auth/login", base_url)) + .json(&json!({ + "username": "user1", + "password": "pass1" + })) + .send() + .await; + + let login2 = client2 + .post(format!("{}/api/auth/login", base_url)) + .json(&json!({ + "username": "user2", + "password": "pass2" + })) + .send() + .await; + + if let (Ok(resp1), Ok(resp2)) = (login1, login2) { + if resp1.status() == 200 && resp2.status() == 200 { + let body1: serde_json::Value = resp1.json().await.unwrap_or_default(); + let body2: serde_json::Value = resp2.json().await.unwrap_or_default(); + + let token1 = body1.get("token").and_then(|t| t.as_str()); + let token2 = body2.get("token").and_then(|t| t.as_str()); + + if let (Some(t1), Some(t2)) = (token1, token2) { + let me1 = client1 + .get(format!("{}/api/users/me", base_url)) + .header("Authorization", format!("Bearer {}", t1)) + .send() + .await; + + let me2 = client2 + .get(format!("{}/api/users/me", base_url)) + .header("Authorization", format!("Bearer {}", t2)) + .send() + .await; + + if let (Ok(r1), Ok(r2)) = (me1, me2) { + if r1.status() == 200 && r2.status() == 200 { + let user1: serde_json::Value = r1.json().await.unwrap_or_default(); + let user2: serde_json::Value = r2.json().await.unwrap_or_default(); + + let id1 = user1.get("id"); + let id2 = user2.get("id"); + + if let (Some(i1), Some(i2)) = (id1, id2) { + assert_ne!( + i1, i2, + "Different users should have different IDs" + ); + } + } + } + } + } + } +} + +#[tokio::test] +async fn test_password_complexity_validation() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .post(format!("{}/api/auth/register", base_url)) + .json(&json!({ + "username": "newuser", + "email": "newuser@example.com", + "password": "weak" + })) + .send() + .await; + + if let Ok(resp) = response { + assert!( + resp.status() == 400 || resp.status() == 422 || resp.status() == 409, + "Weak password should be rejected" + ); + } +} + +#[tokio::test] +async fn test_failed_login_lockout() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + for _ in 0..5 { + let _ = client + .post(format!("{}/api/auth/login", base_url)) + .json(&json!({ + "username": "locktest", + "password": "wrongpassword" + })) + .send() + .await; + } + + let response = client + .post(format!("{}/api/auth/login", base_url)) + .json(&json!({ + "username": "locktest", + "password": "correctpassword" + })) + .send() + .await; + + if let Ok(resp) = response { + if resp.status() == 423 { + assert!(true, "Account should be locked after failed attempts"); + } else if resp.status() == 200 || resp.status() == 401 { + eprintln!("Note: Account lockout may not be enabled"); + } + } +} + +#[tokio::test] +async fn test_access_logging() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let _ = client + .get(format!("{}/api/sensitive-data", base_url)) + .send() + .await; + + eprintln!("Note: Access logging verification requires log inspection"); +} diff --git a/tests/integration/internationalization.rs b/tests/integration/internationalization.rs new file mode 100644 index 0000000..fad044d --- /dev/null +++ b/tests/integration/internationalization.rs @@ -0,0 +1,377 @@ +mod internationalization; + +use bottest::prelude::*; +use reqwest::Client; +use serde_json::json; +use std::time::Duration; + +fn test_client() -> Client { + Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .expect("Failed to create HTTP client") +} + +fn external_server_url() -> Option<String> { + std::env::var("BOTSERVER_URL").ok() +} + +async fn get_test_server() -> Option<(Option<TestContext>, String)> { + if let Some(url) = external_server_url() { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(2)) + .build() + .ok()?; + + if client.get(&url).send().await.is_ok() { + return Some((None, url)); + } + } + + let ctx = TestHarness::quick().await.ok()?; + let server = ctx.start_botserver().await.ok()?; + + if server.is_running() { + Some((Some(ctx), server.url.clone())) + } else { + None + } +} + +macro_rules! skip_if_no_server { + ($base_url:expr) => { + if $base_url.is_none() { + eprintln!("Skipping test: no server available"); + return; + } + }; +} + +#[tokio::test] +async fn test_accept_language_header() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/suite/auth/login.html", base_url)) + .header("Accept-Language", "es") + .send() + .await; + + match response { + Ok(resp) => { + assert!( + resp.status().is_success(), + "Server should respond to requests with Accept-Language header" + ); + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_locale_parameter_support() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let locales = vec!["en", "es", "pt-BR", "fr", "de"]; + + for locale in locales { + let response = client + .get(format!("{}/suite/auth/login.html?locale={}", base_url, locale)) + .send() + .await; + + match response { + Ok(resp) => { + assert!( + resp.status().is_success() || resp.status() == 404, + "Server should handle locale parameter: {}", + locale + ); + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } + } +} + +#[tokio::test] +async fn test_content_language_header() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/health", base_url)) + .send() + .await; + + match response { + Ok(resp) => { + let has_content_lang = resp + .headers() + .contains_key("content-language"); + + if !has_content_lang { + eprintln!("Note: Content-Language header not set - i18n may not be fully configured"); + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_utf8_encoding() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/suite/auth/login.html", base_url)) + .send() + .await; + + match response { + Ok(resp) => { + let content_type = resp + .headers() + .get("content-type") + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + assert!( + content_type.contains("charset=utf-8") || content_type.contains("UTF-8"), + "Response should specify UTF-8 encoding for i18n support" + ); + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_multilingual_error_messages() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let test_cases = vec![ + (("Accept-Language", "en"), "error"), + (("Accept-Language", "es"), "error"), + (("Accept-Language", "pt-BR"), "erro"), + ]; + + for ((lang_header, lang_value), expected_word) in test_cases { + let response = client + .post(format!("{}/api/auth/login", base_url)) + .header(lang_header, lang_value) + .json(&json!({ + "username": "", + "password": "" + })) + .send() + .await; + + match response { + Ok(resp) => { + if resp.status() == 400 || resp.status() == 401 || resp.status() == 422 { + let body = resp.text().await.unwrap_or_default().to_lowercase(); + + assert!( + body.contains(expected_word), + "Error response for {} should contain '{}'", + lang_value, + expected_word + ); + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } + } +} + +#[tokio::test] +async fn test_date_format_localization() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/api/users/me", base_url)) + .header("Accept-Language", "pt-BR") + .send() + .await; + + match response { + Ok(resp) => { + if resp.status() == 200 { + let body = resp.text().await.unwrap_or_default(); + + if let Ok(json) = serde_json::from_str::<serde_json::Value>(&body) { + if let Some(created_at) = json.get("created_at") { + assert!( + created_at.is_string(), + "Date fields should be properly formatted" + ); + } + } + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_number_format_localization() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/api/stats", base_url)) + .send() + .await; + + match response { + Ok(resp) => { + if resp.status() == 200 { + let body = resp.text().await.unwrap_or_default(); + + assert!( + body.contains("0") || body.contains("1"), + "Stats should return numeric data" + ); + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_rtl_language_support() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let rtl_languages = vec!["ar", "he", "fa"]; + + for lang in rtl_languages { + let response = client + .get(format!("{}/suite/auth/login.html", base_url)) + .header("Accept-Language", lang) + .send() + .await; + + match response { + Ok(resp) => { + if resp.status().is_success() { + let body = resp.text().await.unwrap_or_default(); + + let has_dir_attr = body.contains("dir=\"rtl\"") + || body.contains("dir='rtl'") + || body.contains("direction: rtl"); + + if !has_dir_attr { + eprintln!( + "Note: RTL support for {} may not be implemented", + lang + ); + } + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } + } +} + +#[tokio::test] +async fn test_translation_files_exist() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let locales = vec!["en", "es", "pt-BR", "fr", "de", "zh"]; + + for locale in locales { + let response = client + .get(format!("{}/locales/{}/messages.json", base_url, locale)) + .send() + .await; + + match response { + Ok(resp) => { + if resp.status() == 404 { + eprintln!( + "Note: Translation file for locale '{}' may not exist", + locale + ); + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } + } +} + +#[tokio::test] +async fn test_fallback_to_default_locale() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/suite/auth/login.html", base_url)) + .header("Accept-Language", "xyz-UNKNOWN") + .send() + .await; + + match response { + Ok(resp) => { + assert!( + resp.status().is_success(), + "Server should fall back to default locale for unknown languages" + ); + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} diff --git a/tests/integration/mod.rs b/tests/integration/mod.rs index 5cdd685..e540f8e 100644 --- a/tests/integration/mod.rs +++ b/tests/integration/mod.rs @@ -1,6 +1,11 @@ mod api; mod basic_runtime; mod database; +mod security; +mod performance; +mod compliance; +mod accessibility; +mod internationalization; use bottest::prelude::*; diff --git a/tests/integration/performance.rs b/tests/integration/performance.rs new file mode 100644 index 0000000..f8db88a --- /dev/null +++ b/tests/integration/performance.rs @@ -0,0 +1,199 @@ +mod performance; + +use bottest::prelude::*; +use reqwest::Client; +use std::time::{Duration, Instant}; + +fn test_client() -> Client { + Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .expect("Failed to create HTTP client") +} + +fn external_server_url() -> Option<String> { + std::env::var("BOTSERVER_URL").ok() +} + +async fn get_test_server() -> Option<(Option<TestContext>, String)> { + if let Some(url) = external_server_url() { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(2)) + .build() + .ok()?; + + if client.get(&url).send().await.is_ok() { + return Some((None, url)); + } + } + + let ctx = TestHarness::quick().await.ok()?; + let server = ctx.start_botserver().await.ok()?; + + if server.is_running() { + Some((Some(ctx), server.url.clone())) + } else { + None + } +} + +macro_rules! skip_if_no_server { + ($base_url:expr) => { + if $base_url.is_none() { + eprintln!("Skipping test: no server available"); + return; + } + }; +} + +#[tokio::test] +async fn test_concurrent_requests_handled() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let handles: Vec<_> = (0..20) + .map(|_| { + let client = client.clone(); + let base_url = base_url.clone(); + tokio::spawn(async move { + client + .get(format!("{}/health", base_url)) + .send() + .await + }) + }) + .collect(); + + let results = futures::future::join_all(handles).await; + + let successes = results + .iter() + .filter(|r| r.as_ref().map(|resp| resp.status().is_success()).unwrap_or(false)) + .count(); + + assert!( + successes >= 15, + "At least 75% of concurrent requests should succeed" + ); +} + +#[tokio::test] +async fn test_response_time_acceptable() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let start = Instant::now(); + + let response = client + .get(format!("{}/health", base_url)) + .send() + .await; + + let elapsed = start.elapsed(); + + match response { + Ok(resp) => { + assert!(resp.status().is_success(), "Health should return success"); + assert!( + elapsed < Duration::from_secs(5), + "Response time should be under 5s, got {:?}", + elapsed + ); + } + Err(e) => { + eprintln!("Skipping: request failed: {}", e); + } + } +} + +#[tokio::test] +async fn test_sustained_load() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let mut successes = 0; + let total_requests = 30; + + for _ in 0..total_requests { + let response = client + .get(format!("{}/health", base_url)) + .send() + .await; + + if let Ok(resp) = response { + if resp.status().is_success() { + successes += 1; + } + } + + tokio::time::sleep(Duration::from_millis(50)).await; + } + + let success_rate = (successes as f64 / total_requests as f64) * 100.0; + assert!( + success_rate >= 80.0, + "Success rate should be >= 80%, got {:.1}%", + success_rate + ); +} + +#[tokio::test] +async fn test_cache_improves_performance() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let start1 = Instant::now(); + let _ = client + .get(format!("{}/version", base_url)) + .send() + .await; + let first_request = start1.elapsed(); + + tokio::time::sleep(Duration::from_millis(50)).await; + + let start2 = Instant::now(); + let _ = client + .get(format!("{}/version", base_url)) + .send() + .await; + let second_request = start2.elapsed(); + + if second_request < first_request { + assert!( + second_request < first_request * 8 / 10, + "Second request should be significantly faster due to caching" + ); + } else { + eprintln!("Note: Caching may not be enabled or working"); + } +} + +#[tokio::test] +async fn test_memory_stability() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + for _ in 0..20 { + let _ = client + .get(format!("{}/health", base_url)) + .send() + .await; + } + + eprintln!("Note: Memory profiling requires external monitoring"); +} diff --git a/tests/integration/security.rs b/tests/integration/security.rs new file mode 100644 index 0000000..01deb04 --- /dev/null +++ b/tests/integration/security.rs @@ -0,0 +1,276 @@ +mod security; + +use bottest::prelude::*; +use reqwest::Client; +use serde_json::json; +use std::time::Duration; + +fn test_client() -> Client { + Client::builder() + .timeout(Duration::from_secs(30)) + .build() + .expect("Failed to create HTTP client") +} + +fn external_server_url() -> Option<String> { + std::env::var("BOTSERVER_URL").ok() +} + +async fn get_test_server() -> Option<(Option<TestContext>, String)> { + if let Some(url) = external_server_url() { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(2)) + .build() + .ok()?; + + if client.get(&url).send().await.is_ok() { + return Some((None, url)); + } + } + + let ctx = TestHarness::quick().await.ok()?; + let server = ctx.start_botserver().await.ok()?; + + if server.is_running() { + Some((Some(ctx), server.url.clone())) + } else { + None + } +} + +macro_rules! skip_if_no_server { + ($base_url:expr) => { + if $base_url.is_none() { + eprintln!("Skipping test: no server available"); + return; + } + }; +} + +#[tokio::test] +async fn test_invalid_credentials_rejected() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .post(format!("{}/api/auth/login", base_url)) + .json(&json!({ + "username": "admin", + "password": "wrongpassword" + })) + .send() + .await; + + match response { + Ok(resp) => { + assert!( + resp.status() == 401 || resp.status() == 400, + "Invalid credentials should be rejected" + ); + } + Err(e) => { + eprintln!("Skipping: connection failed: {}", e); + } + } +} + +#[tokio::test] +async fn test_session_timeout() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let login_response = client + .post(format!("{}/api/auth/login", base_url)) + .json(&json!({ + "username": "admin", + "password": "admin" + })) + .send() + .await; + + if let Ok(resp) = login_response { + if resp.status() == 200 { + tokio::time::sleep(Duration::from_secs(31)).await; + + let body: serde_json::Value = resp.json().await.unwrap_or_default(); + if let Some(token) = body.get("token").and_then(|t| t.as_str()) { + let protected_response = client + .get(format!("{}/api/users/me", base_url)) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await; + + if let Ok(resp) = protected_response { + assert!( + resp.status() == 401, + "Expired token should be rejected" + ); + } + } + } + } +} + +#[tokio::test] +async fn test_rate_limiting_applied() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let mut rate_limited = false; + + for _ in 0..20 { + let response = client + .post(format!("{}/api/auth/login", base_url)) + .json(&json!({ + "username": "testuser", + "password": "testpass" + })) + .send() + .await; + + if let Ok(resp) = response { + if resp.status() == 429 { + rate_limited = true; + break; + } + } + } + + if !rate_limited { + eprintln!("Note: Rate limiting may not be enabled in test environment"); + } +} + +#[tokio::test] +async fn test_sql_injection_blocked() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/api/users?filter='; DROP TABLE users; --", base_url)) + .send() + .await; + + match response { + Ok(resp) => { + assert!( + resp.status() == 400 || resp.status() == 401 || resp.status() == 403, + "SQL injection attempt should be blocked" + ); + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_xss_prevention() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let malicious_input = "<script>alert('xss')</script>"; + + let response = client + .post(format!("{}/api/users", base_url)) + .json(&json!({ + "name": malicious_input, + "email": "test@example.com" + })) + .send() + .await; + + match response { + Ok(resp) => { + assert!( + resp.status() == 400 || resp.status() == 401 || resp.status() == 422, + "XSS input should be rejected" + ); + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +} + +#[tokio::test] +async fn test_admin_routes_require_admin() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let login_response = client + .post(format!("{}/api/auth/login", base_url)) + .json(&json!({ + "username": "user", + "password": "user" + })) + .send() + .await; + + if let Ok(resp) = login_response { + if resp.status() == 200 { + let body: serde_json::Value = resp.json().await.unwrap_or_default(); + if let Some(token) = body.get("token").and_then(|t| t.as_str()) { + let admin_response = client + .get(format!("{}/api/admin/users", base_url)) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await; + + if let Ok(resp) = admin_response { + assert!( + resp.status() == 403 || resp.status() == 401, + "Non-admin should not access admin routes" + ); + } + } + } + } +} + +#[tokio::test] +async fn test_security_headers_present() { + let server = get_test_server().await; + skip_if_no_server!(server); + + let (_ctx, base_url) = server.unwrap(); + let client = test_client(); + + let response = client + .get(format!("{}/health", base_url)) + .send() + .await; + + match response { + Ok(resp) => { + let headers = resp.headers(); + let has_strict_transport = headers.contains_key("strict-transport-security"); + let has_x_content_type = headers.contains_key("x-content-type-options"); + + if !has_strict_transport || !has_x_content_type { + eprintln!("Note: Security headers may not be fully configured"); + } + } + Err(_) => { + eprintln!("Skipping: server not available"); + } + } +}