Add integration tests for accessibility, compliance, and internationalization

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-03-16 08:15:52 -03:00
parent f6f4e5d2e4
commit 68f52ff326
7 changed files with 1632 additions and 0 deletions

View file

@ -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());
}
}

View file

@ -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<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_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("<title>") || 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");
}
}
}

View file

@ -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");
}

View file

@ -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");
}
}
}

View file

@ -1,6 +1,11 @@
mod api;
mod basic_runtime;
mod database;
mod security;
mod performance;
mod compliance;
mod accessibility;
mod internationalization;
use bottest::prelude::*;

View file

@ -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");
}

View file

@ -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");
}
}
}