//! WP-01: /api/users routes, admin only. mod common; use axum::http::StatusCode; use common::{get, patch, post, test_app_with_admin}; use serde_json::json; const ADMIN: &str = "admin@example.com"; const PW: &str = "admin-password-123"; fn new_user(email: &str) -> serde_json::Value { json!({"email": email, "display_name": "Test User", "password": "user-password-123", "role": "user"}) } #[tokio::test] async fn admin_can_create_list_get_and_update_users() { let app = test_app_with_admin().await; let token = common::login(&app, ADMIN, PW).await.access; let res = post(&app, "/api/users", new_user("u@x.de"), Some(&token)).await; assert_eq!(res.status, StatusCode::CREATED, "{}", res.json); let id = res.json["id"].as_str().unwrap().to_string(); assert_eq!(res.json["role"], "user"); assert_eq!(res.json["is_active"], true); assert!(res.json.get("password_hash").is_none()); let list = get(&app, "/api/users", Some(&token)).await; assert_eq!(list.json.as_array().unwrap().len(), 2); let one = get(&app, &format!("/api/users/{id}"), Some(&token)).await; assert_eq!(one.json["email"], "u@x.de"); let upd = patch( &app, &format!("/api/users/{id}"), json!({"display_name": "Renamed", "role": "admin"}), Some(&token), ) .await; assert_eq!(upd.status, StatusCode::OK); assert_eq!(upd.json["display_name"], "Renamed"); assert_eq!(upd.json["role"], "admin"); } #[tokio::test] async fn validation_and_conflict_errors_are_mapped() { let app = test_app_with_admin().await; let token = common::login(&app, ADMIN, PW).await.access; let res = post(&app, "/api/users", json!({"email": "bad", "display_name": "x", "password": "user-password-123", "role": "user"}), Some(&token)).await; assert_eq!(res.status, StatusCode::UNPROCESSABLE_ENTITY); assert_eq!(res.json["error"], "validation"); let res = post(&app, "/api/users", new_user(ADMIN), Some(&token)).await; assert_eq!(res.status, StatusCode::CONFLICT); assert_eq!(res.json["error"], "email_taken"); let res = get( &app, "/api/users/00000000-0000-0000-0000-000000000000", Some(&token), ) .await; assert_eq!(res.status, StatusCode::NOT_FOUND); } #[tokio::test] async fn last_admin_cannot_be_deactivated() { let app = test_app_with_admin().await; let login = common::login(&app, ADMIN, PW).await; let res = patch( &app, &format!("/api/users/{}", login.user_id), json!({"is_active": false}), Some(&login.access), ) .await; assert_eq!(res.status, StatusCode::CONFLICT); assert_eq!(res.json["error"], "last_admin"); } #[tokio::test] async fn admin_can_reset_password_and_user_can_login_with_it() { let app = test_app_with_admin().await; let token = common::login(&app, ADMIN, PW).await.access; let id = post(&app, "/api/users", new_user("u@x.de"), Some(&token)) .await .json["id"] .as_str() .unwrap() .to_string(); let res = post( &app, &format!("/api/users/{id}/password"), json!({"password": "brand-new-password"}), Some(&token), ) .await; assert_eq!(res.status, StatusCode::NO_CONTENT); let res = post( &app, "/api/auth/login", json!({"email": "u@x.de", "password": "brand-new-password"}), None, ) .await; assert_eq!(res.status, StatusCode::OK); } #[tokio::test] async fn non_admin_gets_403_on_user_management() { let app = test_app_with_admin().await; let admin = common::login(&app, ADMIN, PW).await.access; post(&app, "/api/users", new_user("u@x.de"), Some(&admin)).await; let user = common::login(&app, "u@x.de", "user-password-123") .await .access; assert_eq!( get(&app, "/api/users", Some(&user)).await.status, StatusCode::FORBIDDEN ); assert_eq!( post(&app, "/api/users", new_user("v@x.de"), Some(&user)) .await .status, StatusCode::FORBIDDEN ); // but the user can see themself assert_eq!( get(&app, "/api/auth/me", Some(&user)).await.json["email"], "u@x.de" ); } #[tokio::test] async fn unauthenticated_requests_are_401() { let app = test_app_with_admin().await; assert_eq!( get(&app, "/api/users", None).await.status, StatusCode::UNAUTHORIZED ); }