Files
nx9-auth/tests/password_reset_api.rs
T

240 lines
7.3 KiB
Rust

use axum::{
body::Body,
http::{Request, StatusCode, header},
};
use http_body_util::BodyExt;
use serde_json::Value;
use tower::ServiceExt;
use nx9_auth::{
api,
config::{Config, SecurityConfig},
db::models::Tenant,
identity::users as identity_users,
state::AppState,
};
fn test_security_config() -> SecurityConfig {
SecurityConfig {
session_ttl_hours: 24,
session_absolute_ttl_days: 30,
token_ttl_days: 365,
argon2_memory: 4096,
argon2_iterations: 1,
argon2_parallelism: 1,
}
}
async fn setup() -> (AppState, String, String) {
let db_id = uuid::Uuid::new_v4().to_string();
let db_path = format!("target/test_pwdreset_{}.db", db_id);
let pool = nx9_auth::db::create_pool(&db_path).await.unwrap();
nx9_auth::db::run_migrations(&pool).await.unwrap();
let provider: std::sync::Arc<dyn nx9_auth::db::provider::DatabaseProvider> =
std::sync::Arc::new(nx9_auth::db::provider::SqliteProvider::new(pool));
let mut config = Config {
security: test_security_config(),
..Default::default()
};
config.server.host = "127.0.0.1".into();
config.server.port = 8655;
let state = AppState::new(provider.clone(), config);
let admin = identity_users::create_user(
&state.provider,
&test_security_config(),
Tenant::DEFAULT_ID,
"admin_pw",
"S3cur3#P@ssw0rd$N0S3qu3nc3!",
None,
None,
None,
)
.await
.unwrap();
let admin_role = state
.provider
.roles()
.find_by_name("admin")
.await
.unwrap()
.unwrap();
state
.provider
.roles()
.assign_to_user(&admin.id, &admin_role.id)
.await
.unwrap();
(state, db_path, admin.id)
}
async fn login_cookie(app: axum::Router, user: &str, pass: &str) -> String {
let app = app;
let res = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/auth/login")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(format!(
r#"{{"username":"{user}","password":"{pass}"}}"#
)))
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK, "login failed");
let set_cookie = res
.headers()
.get_all(header::SET_COOKIE)
.iter()
.filter_map(|v| v.to_str().ok())
.find(|c| c.starts_with("nx9_session="))
.expect("session cookie")
.to_string();
set_cookie.split(';').next().unwrap().to_string()
}
#[tokio::test]
async fn test_api_profile_change_password() {
let (state, db_path, _admin_id) = setup().await;
let app = api::router::build(state.clone());
let cookie = login_cookie(app.clone(), "admin_pw", "S3cur3#P@ssw0rd$N0S3qu3nc3!").await;
let res = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/profile/password")
.header(header::CONTENT_TYPE, "application/json")
.header(header::COOKIE, &cookie)
.body(Body::from(
r#"{"current_password":"S3cur3#P@ssw0rd$N0S3qu3nc3!","new_password":"brand_new_admin_pass_456"}"#,
))
.unwrap(),
)
.await
.unwrap();
let status = res.status();
let body = res.into_body().collect().await.unwrap().to_bytes();
let text = String::from_utf8_lossy(&body);
assert_eq!(status, StatusCode::OK, "change password failed: {text}");
// login with new password
let app = api::router::build(state);
let res = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/auth/login")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(
r#"{"username":"admin_pw","password":"brand_new_admin_pass_456"}"#,
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let _ = std::fs::remove_file(db_path);
}
#[tokio::test]
async fn test_api_admin_reset_password() {
let (state, db_path, _admin_id) = setup().await;
let _pool = state.provider.clone();
let target = identity_users::create_user(
&state.provider,
&test_security_config(),
Tenant::DEFAULT_ID,
"target_user",
"super_secure_passphrase_123",
None,
None,
None,
)
.await
.unwrap();
let app = api::router::build(state.clone());
let cookie = login_cookie(app.clone(), "admin_pw", "S3cur3#P@ssw0rd$N0S3qu3nc3!").await;
let res = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/api/v1/users/{}/reset-password", target.id))
.header(header::CONTENT_TYPE, "application/json")
.header(header::COOKIE, &cookie)
.body(Body::from(r#"{"password":"reset_to_new_secure_phrase"}"#))
.unwrap(),
)
.await
.unwrap();
let status = res.status();
let body = res.into_body().collect().await.unwrap().to_bytes();
let text = String::from_utf8_lossy(&body);
assert_eq!(status, StatusCode::OK, "reset password failed: {text}");
// target can login with new password
let app = api::router::build(state);
let res = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/auth/login")
.header(header::CONTENT_TYPE, "application/json")
.body(Body::from(
r#"{"username":"target_user","password":"reset_to_new_secure_phrase"}"#,
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
let _ = std::fs::remove_file(db_path);
}
#[tokio::test]
async fn test_api_reset_password_weak_returns_422() {
let (state, db_path, _admin_id) = setup().await;
let _pool = state.provider.clone();
let target = identity_users::create_user(
&state.provider,
&test_security_config(),
Tenant::DEFAULT_ID,
"target_weak",
"super_secure_passphrase_123",
None,
None,
None,
)
.await
.unwrap();
let app = api::router::build(state);
let cookie = login_cookie(app.clone(), "admin_pw", "S3cur3#P@ssw0rd$N0S3qu3nc3!").await;
let res = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/api/v1/users/{}/reset-password", target.id))
.header(header::CONTENT_TYPE, "application/json")
.header(header::COOKIE, &cookie)
.body(Body::from(r#"{"password":"password12345"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let body = res.into_body().collect().await.unwrap().to_bytes();
let json: Value = serde_json::from_slice(&body).unwrap();
assert!(
json["error"].as_str().unwrap().contains("weak")
|| json["error"].as_str().unwrap().contains("password")
);
let _ = std::fs::remove_file(db_path);
}