mod common; use axum::http::StatusCode; use common::*; // ═══════════════════════════════════════════════════════════════════ // Role management: admin can change another user's role // ═══════════════════════════════════════════════════════════════════ #[tokio::test] async fn admin_can_change_user_role_to_admin() { let (app, pool) = build_test_app().await; let admin = admin_token(&app, &pool, "rolechangeadmin").await; let user_token = register_token(&app, "targetroleuser").await; // Get user's account ID let (_, me) = send(&app, get("/api/v1/auth/me", &user_token)).await; let user_id = me["id"].as_str().unwrap(); // Admin promotes user to admin let (status, body) = send( &app, patch( &format!("/api/v1/accounts/{user_id}"), &admin, serde_json::json!({ "role": "admin" }), ), ).await; assert_eq!(status, StatusCode::OK, "admin should be able to change role: {body}"); assert_eq!(body["role"], "admin"); } #[tokio::test] async fn admin_can_demote_user_role() { let (app, pool) = build_test_app().await; let admin = admin_token(&app, &pool, "demoteadmin").await; // Create a second admin let second_admin = admin_token(&app, &pool, "secondadmin").await; let (_, me) = send(&app, get("/api/v1/auth/me", &second_admin)).await; let second_id = me["id"].as_str().unwrap(); // Demote to user let (status, body) = send( &app, patch( &format!("/api/v1/accounts/{second_id}"), &admin, serde_json::json!({ "role": "user" }), ), ).await; assert_eq!(status, StatusCode::OK, "admin should be able to demote: {body}"); assert_eq!(body["role"], "user"); } // ═══════════════════════════════════════════════════════════════════ // Security: user cannot escalate own role via self-update // ═══════════════════════════════════════════════════════════════════ #[tokio::test] async fn user_cannot_escalate_role_via_self_update() { let (app, _pool) = build_test_app().await; let token = register_token(&app, "escalateuser").await; let (_, me) = send(&app, get("/api/v1/auth/me", &token)).await; let user_id = me["id"].as_str().unwrap(); // Try to escalate role to admin let (status, body) = send( &app, patch( &format!("/api/v1/accounts/{user_id}"), &token, serde_json::json!({ "role": "admin", "display_name": "Updated" }), ), ).await; // The update may succeed but role should be ignored if status == StatusCode::OK { // Verify role was NOT changed let (_, updated) = send(&app, get(&format!("/api/v1/accounts/{user_id}"), &token)).await; assert_ne!(updated["role"], "admin", "user should NOT be able to escalate own role: {updated}"); assert_eq!(updated["display_name"], "Updated", "display_name should still update"); } // If the endpoint rejects the role field entirely, that's also acceptable } // ═══════════════════════════════════════════════════════════════════ // Account status management: admin can disable/enable // ═══════════════════════════════════════════════════════════════════ #[tokio::test] async fn admin_can_disable_account() { let (app, pool) = build_test_app().await; let admin = admin_token(&app, &pool, "disableadmin").await; let user_token = register_token(&app, "disabletarget").await; let (_, me) = send(&app, get("/api/v1/auth/me", &user_token)).await; let user_id = me["id"].as_str().unwrap(); // Admin disables user let (status, body) = send( &app, patch( &format!("/api/v1/accounts/{user_id}/status"), &admin, serde_json::json!({ "status": "disabled" }), ), ).await; assert_eq!(status, StatusCode::OK, "admin should be able to disable account: {body}"); // Disabled user cannot log in let (status, _) = send( &app, post_public( "/api/v1/auth/login", serde_json::json!({ "username": "disabletarget", "password": DEFAULT_PASSWORD }), ), ).await; assert_eq!(status, StatusCode::FORBIDDEN, "disabled account should not be able to log in"); } #[tokio::test] async fn admin_can_reenable_account() { let (app, pool) = build_test_app().await; let admin = admin_token(&app, &pool, "reenableadmin").await; register(&app, "reenabletarget", "reenable@test.io", DEFAULT_PASSWORD).await; // Disable first let (_, me) = send(&app, get("/api/v1/auth/me", &admin)).await; // Get the target user's ID from accounts list let (_, list) = send(&app, get("/api/v1/accounts", &admin)).await; let items = list["items"].as_array().expect("accounts list"); let target_id = items.iter() .find(|a| a["username"] == "reenabletarget") .map(|a| a["id"].as_str().unwrap()) .expect("should find target user"); // Disable send( &app, patch( &format!("/api/v1/accounts/{target_id}/status"), &admin, serde_json::json!({ "status": "disabled" }), ), ).await; // Re-enable let (status, _) = send( &app, patch( &format!("/api/v1/accounts/{target_id}/status"), &admin, serde_json::json!({ "status": "active" }), ), ).await; assert_eq!(status, StatusCode::OK, "admin should be able to re-enable account"); // User can log in again let (status, _) = send( &app, post_public( "/api/v1/auth/login", serde_json::json!({ "username": "reenabletarget", "password": DEFAULT_PASSWORD }), ), ).await; assert_eq!(status, StatusCode::OK, "re-enabled account should be able to log in"); } #[tokio::test] async fn user_cannot_disable_others() { let (app, _pool) = build_test_app().await; let token = register_token(&app, "disableattacker").await; let victim_token = register_token(&app, "disablevictim").await; let (_, me) = send(&app, get("/api/v1/auth/me", &victim_token)).await; let victim_id = me["id"].as_str().unwrap(); let (status, _) = send( &app, patch( &format!("/api/v1/accounts/{victim_id}/status"), &token, serde_json::json!({ "status": "disabled" }), ), ).await; assert_eq!(status, StatusCode::FORBIDDEN, "regular user should not be able to disable others"); } // ═══════════════════════════════════════════════════════════════════ // Cross-account access control // ═══════════════════════════════════════════════════════════════════ #[tokio::test] async fn user_cannot_view_other_account_details() { let (app, _pool) = build_test_app().await; let token_a = register_token(&app, "viewerA").await; let token_b = register_token(&app, "viewerB").await; let (_, me_b) = send(&app, get("/api/v1/auth/me", &token_b)).await; let b_id = me_b["id"].as_str().unwrap(); let (status, _) = send(&app, get(&format!("/api/v1/accounts/{b_id}"), &token_a)).await; assert_eq!(status, StatusCode::FORBIDDEN, "user should not see another user's account details"); } #[tokio::test] async fn admin_can_view_any_account() { let (app, pool) = build_test_app().await; let admin = admin_token(&app, &pool, "viewadmin").await; let user_token = register_token(&app, "viewtarget").await; let (_, me) = send(&app, get("/api/v1/auth/me", &user_token)).await; let user_id = me["id"].as_str().unwrap(); let (status, body) = send(&app, get(&format!("/api/v1/accounts/{user_id}"), &admin)).await; assert_eq!(status, StatusCode::OK, "admin should be able to view any account: {body}"); assert_eq!(body["username"], "viewtarget"); } // ═══════════════════════════════════════════════════════════════════ // Operation logs are admin-only // ═══════════════════════════════════════════════════════════════════ #[tokio::test] async fn operation_logs_admin_can_list() { let (app, pool) = build_test_app().await; let admin = admin_token(&app, &pool, "logadmin").await; // Perform an action that generates a log send(&app, get("/api/v1/accounts", &admin)).await; let (status, body) = send(&app, get("/api/v1/logs/operations", &admin)).await; assert_eq!(status, StatusCode::OK, "admin should see operation logs: {body}"); // Should be paginated assert!(body["items"].is_array() || body.is_array(), "logs should be array or paginated"); }