bottest/tests/integration/security.rs

276 lines
7.2 KiB
Rust

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