Add integration tests for accessibility, compliance, and internationalization
This commit is contained in:
parent
f6f4e5d2e4
commit
68f52ff326
7 changed files with 1632 additions and 0 deletions
102
tests/email_integration_test.rs
Normal file
102
tests/email_integration_test.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
316
tests/integration/accessibility.rs
Normal file
316
tests/integration/accessibility.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
357
tests/integration/compliance.rs
Normal file
357
tests/integration/compliance.rs
Normal 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");
|
||||||
|
}
|
||||||
377
tests/integration/internationalization.rs
Normal file
377
tests/integration/internationalization.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
mod api;
|
mod api;
|
||||||
mod basic_runtime;
|
mod basic_runtime;
|
||||||
mod database;
|
mod database;
|
||||||
|
mod security;
|
||||||
|
mod performance;
|
||||||
|
mod compliance;
|
||||||
|
mod accessibility;
|
||||||
|
mod internationalization;
|
||||||
|
|
||||||
use bottest::prelude::*;
|
use bottest::prelude::*;
|
||||||
|
|
||||||
|
|
|
||||||
199
tests/integration/performance.rs
Normal file
199
tests/integration/performance.rs
Normal 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");
|
||||||
|
}
|
||||||
276
tests/integration/security.rs
Normal file
276
tests/integration/security.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue