993 lines
41 KiB
Rust
993 lines
41 KiB
Rust
use crate::test_common::{HttpClient, TestContext, login, refresh_token, validate_uuid};
|
|
use crate::test_report::{TestSuite, TestCase, TestStep, TestStatus, Severity};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::time::Instant;
|
|
|
|
pub fn run_auth_tests() -> TestSuite {
|
|
let mut suite = TestSuite::new(
|
|
"ERP-AUTH Module Integration Tests",
|
|
"Complete authentication and authorization flow testing including login, token refresh, logout, user management, role management, and organization management"
|
|
);
|
|
|
|
let runtime = tokio::runtime::Runtime::new().unwrap();
|
|
let client = HttpClient::new("http://localhost:3000");
|
|
|
|
let mut ctx = TestContext::new();
|
|
|
|
runtime.block_on(async {
|
|
test_auth_01_login_success(&client, &mut ctx, &mut suite);
|
|
test_auth_02_login_invalid_password(&client, &mut ctx, &mut suite);
|
|
test_auth_03_login_missing_fields(&client, &mut ctx, &mut suite);
|
|
test_auth_04_sql_injection_attempt(&client, &mut ctx, &mut suite);
|
|
test_auth_05_token_refresh_success(&client, &mut ctx, &mut suite);
|
|
test_auth_06_token_refresh_reuse_detection(&client, &mut ctx, &mut suite);
|
|
test_auth_07_logout_success(&client, &mut ctx, &mut suite);
|
|
test_auth_08_change_password(&client, &mut ctx, &mut suite);
|
|
test_auth_09_change_password_wrong_old(&client, &mut ctx, &mut suite);
|
|
|
|
test_user_01_list_users(&client, &mut ctx, &mut suite);
|
|
test_user_02_list_users_with_pagination(&client, &mut ctx, &mut suite);
|
|
test_user_03_list_users_search(&client, &mut ctx, &mut suite);
|
|
test_user_04_create_user(&client, &mut ctx, &mut suite);
|
|
test_user_05_create_user_duplicate_username(&client, &mut ctx, &mut suite);
|
|
test_user_06_create_user_missing_fields(&client, &mut ctx, &mut suite);
|
|
test_user_07_create_user_weak_password(&client, &mut ctx, &mut suite);
|
|
test_user_08_get_user(&client, &mut ctx, &mut suite);
|
|
test_user_09_get_user_invalid_id(&client, &mut ctx, &mut suite);
|
|
test_user_10_update_user(&client, &mut ctx, &mut suite);
|
|
test_user_11_delete_user(&client, &mut ctx, &mut suite);
|
|
|
|
test_role_01_list_roles(&client, &mut ctx, &mut suite);
|
|
test_role_02_create_role(&client, &mut ctx, &mut suite);
|
|
test_role_03_create_role_duplicate_code(&client, &mut ctx, &mut suite);
|
|
test_role_04_assign_permissions(&client, &mut ctx, &mut suite);
|
|
test_role_05_get_role_permissions(&client, &mut ctx, &mut suite);
|
|
test_role_06_delete_role(&client, &mut ctx, &mut suite);
|
|
|
|
test_org_01_list_organizations(&client, &mut ctx, &mut suite);
|
|
test_org_02_create_organization(&client, &mut ctx, &mut suite);
|
|
test_org_03_create_sub_organization(&client, &mut ctx, &mut suite);
|
|
test_org_04_update_organization(&client, &mut ctx, &mut suite);
|
|
|
|
test_dept_01_create_department(&client, &mut ctx, &mut suite);
|
|
test_dept_02_list_departments(&client, &mut ctx, &mut suite);
|
|
});
|
|
|
|
suite
|
|
}
|
|
|
|
fn test_auth_01_login_success(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("AUTH-01", "Login - Valid Credentials", "Authentication");
|
|
case.severity = Severity::Critical;
|
|
case.expected_result = "HTTP 200, access_token and refresh_token returned".to_string();
|
|
|
|
let step1 = TestStep::new(1, "POST /api/v1/auth/login with valid credentials", "HTTP 200 response");
|
|
let start = Instant::now();
|
|
let result = client.post("/api/v1/auth/login", &serde_json::json!({
|
|
"username": "admin",
|
|
"password": "Admin@2026"
|
|
}), None).await;
|
|
let elapsed = start.elapsed().as_millis() as u64;
|
|
let mut step1 = step1.with_actual(&format!("HTTP {} - {:?}", result.status, result.body), elapsed);
|
|
case.add_step(step1);
|
|
|
|
if result.status == 200 {
|
|
if let Ok(data) = parse_login_response(&result.body) {
|
|
ctx.access_token = data.access_token.clone();
|
|
ctx.refresh_token = data.refresh_token.clone();
|
|
case.actual_result = format!("access_token length: {}, refresh_token length: {}", data.access_token.len(), data.refresh_token.len());
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
}
|
|
} else {
|
|
case.actual_result = format!("HTTP {}, body: {:?}", result.status, result.body);
|
|
case.status = TestStatus::Fail;
|
|
case.error_message = Some("Login failed with valid credentials".to_string());
|
|
}
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_auth_02_login_invalid_password(client: &HttpClient, _ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("AUTH-02", "Login - Invalid Password", "Authentication");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 401 Unauthorized".to_string();
|
|
|
|
let step1 = TestStep::new(1, "POST /api/v1/auth/login with wrong password", "HTTP 401 response");
|
|
let start = Instant::now();
|
|
let result = client.post("/api/v1/auth/login", &serde_json::json!({
|
|
"username": "admin",
|
|
"password": "WrongPassword123!"
|
|
}), None).await;
|
|
let elapsed = start.elapsed().as_millis() as u64;
|
|
let step1 = step1.with_actual(&format!("HTTP {}", result.status), elapsed);
|
|
case.add_step(step1);
|
|
|
|
if result.status == 401 {
|
|
case.actual_result = "HTTP 401 - Authentication failed as expected".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("Expected 401, got {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
case.error_message = Some("Should reject invalid password".to_string());
|
|
}
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_auth_03_login_missing_fields(client: &HttpClient, _ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("AUTH-03", "Login - Missing Required Fields", "Authentication");
|
|
case.severity = Severity::Medium;
|
|
case.expected_result = "HTTP 422 Validation Error".to_string();
|
|
|
|
let result = client.post("/api/v1/auth/login", &serde_json::json!({
|
|
"username": "admin"
|
|
}), None).await;
|
|
|
|
if result.status == 422 || result.status == 400 {
|
|
case.actual_result = format!("HTTP {} - Validation error as expected", result.status);
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("Expected 422, got {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/auth/login with missing password", "HTTP 422/400")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_auth_04_sql_injection_attempt(client: &HttpClient, _ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("AUTH-04", "Login - SQL Injection Attack Prevention", "Security");
|
|
case.severity = Severity::Critical;
|
|
case.expected_result = "HTTP 401 - Attack prevented".to_string();
|
|
|
|
let result = client.post("/api/v1/auth/login", &serde_json::json!({
|
|
"username": "admin' OR 1=1 --",
|
|
"password": "anything"
|
|
}), None).await;
|
|
|
|
if result.status == 401 {
|
|
case.actual_result = "HTTP 401 - SQL injection attack blocked".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("Unexpected response: {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
case.error_message = Some("SQL injection may be possible".to_string());
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/auth/login with SQL injection payload", "HTTP 401")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_auth_05_token_refresh_success(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("AUTH-05", "Token Refresh - Valid Token", "Authentication");
|
|
case.severity = Severity::Critical;
|
|
case.expected_result = "HTTP 200, new access_token and refresh_token returned".to_string();
|
|
|
|
if ctx.refresh_token.is_empty() {
|
|
case.status = TestStatus::Blocked;
|
|
case.error_message = Some("No refresh token available".to_string());
|
|
suite.add_case(case);
|
|
return;
|
|
}
|
|
|
|
let step1 = TestStep::new(1, "POST /api/v1/auth/refresh with valid token", "HTTP 200 with new tokens");
|
|
let start = Instant::now();
|
|
let result = client.post("/api/v1/auth/refresh", &serde_json::json!({
|
|
"refresh_token": ctx.refresh_token
|
|
}), None).await;
|
|
let elapsed = start.elapsed().as_millis() as u64;
|
|
let mut step1 = step1.with_actual(&format!("HTTP {}", result.status), elapsed);
|
|
case.add_step(step1);
|
|
|
|
if result.status == 200 {
|
|
if let Ok(data) = parse_login_response(&result.body) {
|
|
let old_token = ctx.access_token.clone();
|
|
ctx.access_token = data.access_token.clone();
|
|
ctx.refresh_token = data.refresh_token.clone();
|
|
if old_token != ctx.access_token {
|
|
case.actual_result = "Token rotated successfully".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = "Token did not rotate".to_string();
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
}
|
|
} else {
|
|
case.actual_result = format!("HTTP {} - {:?}", result.status, result.body);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_auth_06_token_refresh_reuse_detection(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("AUTH-06", "Token Refresh - Reuse Detection (Rotation Security)", "Security");
|
|
case.severity = Severity::Critical;
|
|
case.expected_result = "Second use of old refresh token should be rejected".to_string();
|
|
|
|
if ctx.refresh_token.is_empty() {
|
|
case.status = TestStatus::Blocked;
|
|
suite.add_case(case);
|
|
return;
|
|
}
|
|
|
|
let old_refresh = ctx.refresh_token.clone();
|
|
|
|
let result1 = client.post("/api/v1/auth/refresh", &serde_json::json!({
|
|
"refresh_token": old_refresh
|
|
}), None).await;
|
|
|
|
if result1.status == 200 {
|
|
if let Ok(data) = parse_login_response(&result1.body) {
|
|
ctx.refresh_token = data.refresh_token;
|
|
}
|
|
}
|
|
|
|
let result2 = client.post("/api/v1/auth/refresh", &serde_json::json!({
|
|
"refresh_token": old_refresh
|
|
}), None).await;
|
|
|
|
if result2.status == 401 {
|
|
case.actual_result = "Reused token was rejected - security verified".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("Token reuse not detected: {}", result2.status);
|
|
case.status = TestStatus::Fail;
|
|
case.error_message = Some("Refresh token rotation security failed".to_string());
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "First refresh with token", "HTTP 200")
|
|
.with_actual(&format!("HTTP {}", result1.status), 0));
|
|
case.add_step(TestStep::new(2, "Second refresh with same token (reuse)", "HTTP 401")
|
|
.with_actual(&format!("HTTP {}", result2.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_auth_07_logout_success(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("AUTH-07", "Logout - Success", "Authentication");
|
|
case.severity = Severity::Medium;
|
|
case.expected_result = "HTTP 200 - Token invalidated".to_string();
|
|
|
|
let result = client.post("/api/v1/auth/logout", &serde_json::json!({}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
case.actual_result = "Logout successful".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("HTTP {} - {:?}", result.status, result.body);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/auth/logout", "HTTP 200")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_auth_08_change_password(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("AUTH-08", "Change Password - Success", "Authentication");
|
|
case.severity = Severity::Critical;
|
|
case.expected_result = "HTTP 200 - Password changed".to_string();
|
|
|
|
let result = client.post("/api/v1/auth/change-password", &serde_json::json!({
|
|
"old_password": "Admin@2026",
|
|
"new_password": "NewAdmin@2026!"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
case.actual_result = "Password changed successfully".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("HTTP {} - {:?}", result.status, result.body);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/auth/change-password", "HTTP 200")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_auth_09_change_password_wrong_old(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("AUTH-09", "Change Password - Wrong Old Password", "Authentication");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 400/401 - Old password rejected".to_string();
|
|
|
|
let result = client.post("/api/v1/auth/change-password", &serde_json::json!({
|
|
"old_password": "WrongOldPass",
|
|
"new_password": "AnotherNewPass@2026!"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 400 || result.status == 401 {
|
|
case.actual_result = "Wrong old password rejected".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("Unexpected: {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/auth/change-password with wrong old", "HTTP 400/401")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_user_01_list_users(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("USER-01", "List Users - Basic Pagination", "User Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 200, paginated user list returned".to_string();
|
|
|
|
let result = client.get("/api/v1/users", Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
if let Some((total, page, page_size)) = parse_pagination(&result.body) {
|
|
case.actual_result = format!("total={}, page={}, page_size={}", total, page, page_size);
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = "Invalid pagination format".to_string();
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
} else {
|
|
case.actual_result = format!("HTTP {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "GET /api/v1/users", "HTTP 200 with pagination")
|
|
.with_actual(&format!("HTTP {} - {:?}", result.status, result.body), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_user_02_list_users_with_pagination(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("USER-02", "List Users - Custom Page Size", "User Management");
|
|
case.severity = Severity::Medium;
|
|
case.expected_result = "HTTP 200, page_size=2 returned".to_string();
|
|
|
|
let result = client.get("/api/v1/users?page=1&page_size=2", Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
if let Some((_, page, page_size)) = parse_pagination(&result.body) {
|
|
if page_size == 2 {
|
|
case.actual_result = format!("page_size={} as requested", page_size);
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("Expected page_size=2, got {}", page_size);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
} else {
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
} else {
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "GET /api/v1/users?page=1&page_size=2", "HTTP 200")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_user_03_list_users_search(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("USER-03", "List Users - Search Filter", "User Management");
|
|
case.severity = Severity::Medium;
|
|
case.expected_result = "HTTP 200, filtered results".to_string();
|
|
|
|
let result = client.get("/api/v1/users?search=admin", Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
case.actual_result = "Search filter applied".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("HTTP {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "GET /api/v1/users?search=admin", "HTTP 200")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_user_04_create_user(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("USER-04", "Create User - Valid Data", "User Management");
|
|
case.severity = Severity::Critical;
|
|
case.expected_result = "HTTP 200, user created with valid ID".to_string();
|
|
|
|
let new_username = format!("test_user_{}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap());
|
|
let result = client.post("/api/v1/users", &serde_json::json!({
|
|
"username": new_username,
|
|
"password": "TestPass@2026",
|
|
"display_name": "Test User",
|
|
"email": "test@example.com",
|
|
"phone": "13800138000"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
if let Some(user_id) = parse_id(&result.body) {
|
|
ctx.store_resource("user", uuid::Uuid::parse_str(&user_id).unwrap_or(uuid::Uuid::nil()), &new_username);
|
|
case.actual_result = format!("User created with ID: {}", user_id);
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = "User ID not found in response".to_string();
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
} else {
|
|
case.actual_result = format!("HTTP {} - {:?}", result.status, result.body);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/users", "HTTP 200 with user_id")
|
|
.with_actual(&format!("HTTP {} - {:?}", result.status, result.body), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_user_05_create_user_duplicate_username(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("USER-05", "Create User - Duplicate Username", "User Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 409 Conflict".to_string();
|
|
|
|
let result = client.post("/api/v1/users", &serde_json::json!({
|
|
"username": "test_user_api",
|
|
"password": "TestPass@2026",
|
|
"display_name": "Duplicate User"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 409 || result.status == 400 {
|
|
case.actual_result = "Duplicate username rejected".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("Expected 409/400, got {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/users with duplicate username", "HTTP 409/400")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_user_06_create_user_missing_fields(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("USER-06", "Create User - Missing Required Fields", "User Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 422 Validation Error".to_string();
|
|
|
|
let result = client.post("/api/v1/users", &serde_json::json!({
|
|
"display_name": "No Username User"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 422 || result.status == 400 {
|
|
case.actual_result = "Validation error as expected".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("Expected 422/400, got {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/users with missing fields", "HTTP 422/400")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_user_07_create_user_weak_password(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("USER-07", "Create User - Weak Password Rejection", "User Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 400/422 - Weak password rejected".to_string();
|
|
|
|
let result = client.post("/api/v1/users", &serde_json::json!({
|
|
"username": "weak_pass_user",
|
|
"password": "123",
|
|
"display_name": "Weak Password User"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 400 || result.status == 422 {
|
|
case.actual_result = "Weak password rejected".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("Password policy not enforced: {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/users with weak password", "HTTP 400/422")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_user_08_get_user(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("USER-08", "Get User - By ID", "User Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 200, user data returned".to_string();
|
|
|
|
if let Some(user_id) = ctx.get_resource("user", "test_user_api") {
|
|
let result = client.get(&format!("/api/v1/users/{}", user_id), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
case.actual_result = "User retrieved successfully".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("HTTP {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
} else {
|
|
case.status = TestStatus::Blocked;
|
|
case.error_message = Some("No test user available".to_string());
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "GET /api/v1/users/{id}", "HTTP 200")
|
|
.with_actual("Completed", 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_user_09_get_user_invalid_id(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("USER-09", "Get User - Invalid UUID", "User Management");
|
|
case.severity = Severity::Medium;
|
|
case.expected_result = "HTTP 404 Not Found".to_string();
|
|
|
|
let result = client.get("/api/v1/users/00000000-0000-0000-0000-000000000000", Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 404 {
|
|
case.actual_result = "Invalid UUID handled correctly".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("Expected 404, got {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "GET /api/v1/users/invalid-uuid", "HTTP 404")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_user_10_update_user(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("USER-10", "Update User - Valid Data", "User Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 200, user updated".to_string();
|
|
|
|
if let Some(user_id) = ctx.get_resource("user", "test_user_api") {
|
|
let result = client.put(&format!("/api/v1/users/{}", user_id), &serde_json::json!({
|
|
"display_name": "Updated Test User"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
case.actual_result = "User updated successfully".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("HTTP {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
} else {
|
|
case.status = TestStatus::Blocked;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "PUT /api/v1/users/{id}", "HTTP 200")
|
|
.with_actual("Completed", 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_user_11_delete_user(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("USER-11", "Delete User - Soft Delete", "User Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 200, user soft deleted".to_string();
|
|
|
|
let new_username = format!("delete_test_{}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap());
|
|
let create_result = client.post("/api/v1/users", &serde_json::json!({
|
|
"username": new_username,
|
|
"password": "TestPass@2026",
|
|
"display_name": "Delete Test User"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if create_result.status == 200 {
|
|
if let Some(user_id) = parse_id(&create_result.body) {
|
|
let delete_result = client.delete(&format!("/api/v1/users/{}", user_id), Some(&ctx.access_token)).await;
|
|
|
|
if delete_result.status == 200 {
|
|
case.actual_result = "User soft deleted".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("Delete failed: {}", delete_result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
}
|
|
} else {
|
|
case.status = TestStatus::Blocked;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/users", "HTTP 200")
|
|
.with_actual("Created", 0));
|
|
case.add_step(TestStep::new(2, "DELETE /api/v1/users/{id}", "HTTP 200")
|
|
.with_actual("Completed", 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_role_01_list_roles(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("ROLE-01", "List Roles - Basic", "Role Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 200, role list returned".to_string();
|
|
|
|
let result = client.get("/api/v1/roles", Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
case.actual_result = "Roles listed".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("HTTP {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "GET /api/v1/roles", "HTTP 200")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_role_02_create_role(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("ROLE-02", "Create Role - Valid Data", "Role Management");
|
|
case.severity = Severity::Critical;
|
|
case.expected_result = "HTTP 200, role created".to_string();
|
|
|
|
let role_code = format!("test_role_{}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap());
|
|
let result = client.post("/api/v1/roles", &serde_json::json!({
|
|
"name": "Test Role",
|
|
"code": &role_code,
|
|
"description": "Integration test role"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
if let Some(role_id) = parse_id(&result.body) {
|
|
ctx.store_resource("role", uuid::Uuid::parse_str(&role_id).unwrap_or(uuid::Uuid::nil()), &role_code);
|
|
case.actual_result = format!("Role created: {}", role_id);
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
}
|
|
} else {
|
|
case.actual_result = format!("HTTP {} - {:?}", result.status, result.body);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/roles", "HTTP 200")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_role_03_create_role_duplicate_code(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("ROLE-03", "Create Role - Duplicate Code", "Role Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 409 Conflict".to_string();
|
|
|
|
let result = client.post("/api/v1/roles", &serde_json::json!({
|
|
"name": "Duplicate Role",
|
|
"code": "admin",
|
|
"description": "Should fail"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 409 || result.status == 400 {
|
|
case.actual_result = "Duplicate code rejected".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("Expected 409, got {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/roles with duplicate code", "HTTP 409")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_role_04_assign_permissions(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("ROLE-04", "Assign Permissions to Role", "Role Management");
|
|
case.severity = Severity::Critical;
|
|
case.expected_result = "HTTP 200, permissions assigned".to_string();
|
|
|
|
if let Some(role_id) = ctx.get_resource("role", "test_role_api").or_else(|| {
|
|
ctx.created_resources.get("role").and_then(|r| r.first()).map(|r| r.id)
|
|
}) {
|
|
let result = client.post(&format!("/api/v1/roles/{}/permissions", role_id), &serde_json::json!({
|
|
"permission_codes": ["user.list", "user.create"]
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
case.actual_result = "Permissions assigned".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("HTTP {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
} else {
|
|
case.status = TestStatus::Blocked;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/roles/{id}/permissions", "HTTP 200")
|
|
.with_actual("Completed", 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_role_05_get_role_permissions(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("ROLE-05", "Get Role Permissions", "Role Management");
|
|
case.severity = Severity::Medium;
|
|
case.expected_result = "HTTP 200, permissions list returned".to_string();
|
|
|
|
let result = client.get("/api/v1/roles", Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
case.actual_result = "Permissions retrieved".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("HTTP {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "GET /api/v1/roles", "HTTP 200")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_role_06_delete_role(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("ROLE-06", "Delete Role - Soft Delete", "Role Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 200, role deleted".to_string();
|
|
|
|
let role_code = format!("delete_role_{}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap());
|
|
let create_result = client.post("/api/v1/roles", &serde_json::json!({
|
|
"name": "Delete Test Role",
|
|
"code": &role_code,
|
|
"description": "To be deleted"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if create_result.status == 200 {
|
|
if let Some(role_id) = parse_id(&create_result.body) {
|
|
let delete_result = client.delete(&format!("/api/v1/roles/{}", role_id), Some(&ctx.access_token)).await;
|
|
|
|
if delete_result.status == 200 {
|
|
case.actual_result = "Role deleted".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("Delete failed: {}", delete_result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
}
|
|
} else {
|
|
case.status = TestStatus::Blocked;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/roles", "HTTP 200")
|
|
.with_actual("Created", 0));
|
|
case.add_step(TestStep::new(2, "DELETE /api/v1/roles/{id}", "HTTP 200")
|
|
.with_actual("Completed", 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_org_01_list_organizations(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("ORG-01", "List Organizations - Tree Structure", "Organization Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 200, org tree returned".to_string();
|
|
|
|
let result = client.get("/api/v1/organizations", Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
case.actual_result = "Organization tree retrieved".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("HTTP {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "GET /api/v1/organizations", "HTTP 200")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_org_02_create_organization(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("ORG-02", "Create Organization - Root Level", "Organization Management");
|
|
case.severity = Severity::Critical;
|
|
case.expected_result = "HTTP 200, org created".to_string();
|
|
|
|
let org_name = format!("Test Org {}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap());
|
|
let result = client.post("/api/v1/organizations", &serde_json::json!({
|
|
"name": &org_name,
|
|
"code": format!("ORG_{}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap()),
|
|
"description": "Integration test org"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
if let Some(org_id) = parse_id(&result.body) {
|
|
ctx.store_resource("organization", uuid::Uuid::parse_str(&org_id).unwrap_or(uuid::Uuid::nil()), &org_name);
|
|
case.actual_result = format!("Organization created: {}", org_id);
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
}
|
|
} else {
|
|
case.actual_result = format!("HTTP {} - {:?}", result.status, result.body);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/organizations", "HTTP 200")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_org_03_create_sub_organization(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("ORG-03", "Create Sub-Organization - Hierarchical", "Organization Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 200, sub-org created under parent".to_string();
|
|
|
|
let parent_id = ctx.get_resource("organization", "Test Org");
|
|
if parent_id.is_none() {
|
|
case.status = TestStatus::Blocked;
|
|
case.error_message = Some("No parent org available".to_string());
|
|
suite.add_case(case);
|
|
return;
|
|
}
|
|
|
|
let sub_org_name = format!("Sub Org {}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap());
|
|
let result = client.post("/api/v1/organizations", &serde_json::json!({
|
|
"name": &sub_org_name,
|
|
"code": format!("SUB_{}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap()),
|
|
"parent_id": parent_id,
|
|
"description": "Child organization"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
case.actual_result = "Sub-organization created".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("HTTP {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/organizations with parent_id", "HTTP 200")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_org_04_update_organization(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("ORG-04", "Update Organization", "Organization Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 200, org updated".to_string();
|
|
|
|
if let Some(org_id) = ctx.get_resource("organization", "Test Org") {
|
|
let result = client.put(&format!("/api/v1/organizations/{}", org_id), &serde_json::json!({
|
|
"name": "Updated Test Org"
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
case.actual_result = "Organization updated".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("HTTP {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
} else {
|
|
case.status = TestStatus::Blocked;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "PUT /api/v1/organizations/{id}", "HTTP 200")
|
|
.with_actual("Completed", 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_dept_01_create_department(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("DEPT-01", "Create Department", "Department Management");
|
|
case.severity = Severity::High;
|
|
case.expected_result = "HTTP 200, department created".to_string();
|
|
|
|
let dept_name = format!("Test Dept {}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap());
|
|
let result = client.post("/api/v1/departments", &serde_json::json!({
|
|
"name": &dept_name,
|
|
"code": format!("DEPT_{}", uuid::Uuid::new_v4().to_string().split('-').next().unwrap()),
|
|
"organization_id": ctx.get_resource("organization", "Test Org").or(Some(uuid::Uuid::nil()))
|
|
}), Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
if let Some(dept_id) = parse_id(&result.body) {
|
|
ctx.store_resource("department", uuid::Uuid::parse_str(&dept_id).unwrap_or(uuid::Uuid::nil()), &dept_name);
|
|
case.actual_result = format!("Department created: {}", dept_id);
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
}
|
|
} else {
|
|
case.actual_result = format!("HTTP {} - {:?}", result.status, result.body);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "POST /api/v1/departments", "HTTP 200")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
fn test_dept_02_list_departments(client: &HttpClient, ctx: &mut TestContext, suite: &mut TestSuite) {
|
|
let mut case = TestCase::new("DEPT-02", "List Departments", "Department Management");
|
|
case.severity = Severity::Medium;
|
|
case.expected_result = "HTTP 200, department list returned".to_string();
|
|
|
|
let result = client.get("/api/v1/departments", Some(&ctx.access_token)).await;
|
|
|
|
if result.status == 200 {
|
|
case.actual_result = "Departments listed".to_string();
|
|
case.status = TestStatus::Pass;
|
|
case.business_logic_verified = true;
|
|
} else {
|
|
case.actual_result = format!("HTTP {}", result.status);
|
|
case.status = TestStatus::Fail;
|
|
}
|
|
|
|
case.add_step(TestStep::new(1, "GET /api/v1/departments", "HTTP 200")
|
|
.with_actual(&format!("HTTP {}", result.status), 0));
|
|
|
|
suite.add_case(case);
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct LoginData {
|
|
access_token: String,
|
|
refresh_token: String,
|
|
}
|
|
|
|
fn parse_login_response(body: &serde_json::Value) -> Result<LoginData, String> {
|
|
let data = body.get("data").ok_or("No data field")?;
|
|
Ok(LoginData {
|
|
access_token: data.get("access_token").and_then(|v| v.as_str()).unwrap_or("").to_string(),
|
|
refresh_token: data.get("refresh_token").and_then(|v| v.as_str()).unwrap_or("").to_string(),
|
|
})
|
|
}
|
|
|
|
fn parse_id(body: &serde_json::Value) -> Option<String> {
|
|
body.get("data")?.get("id")?.as_str().map(|s| s.to_string())
|
|
}
|
|
|
|
fn parse_pagination(body: &serde_json::Value) -> Option<(u64, u64, u64)> {
|
|
let data = body.get("data")?;
|
|
Some((
|
|
data.get("total")?.as_u64()?,
|
|
data.get("page")?.as_u64()?,
|
|
data.get("page_size")?.as_u64()?,
|
|
))
|
|
} |