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 { 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 { 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()?, )) }