test reset password

This commit is contained in:
eraden 2024-02-09 06:06:37 +01:00
parent 09d164576f
commit 60c9c915e7
3 changed files with 323 additions and 5 deletions

View File

@ -220,6 +220,7 @@ mod tests {
let req = test::TestRequest::default() let req = test::TestRequest::default()
.insert_header(ContentType::json()) .insert_header(ContentType::json())
.to_request(); .to_request();
let resp = test::call_service(&app, req).await; let resp = test::call_service(&app, req).await;
assert!(resp.status().is_client_error()); assert!(resp.status().is_client_error());
@ -257,6 +258,7 @@ mod tests {
.uri("/users/me/change-password") .uri("/users/me/change-password")
.method(Method::POST) .method(Method::POST)
.to_request(); .to_request();
let resp = test::call_service(&app, req).await; let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
@ -320,6 +322,7 @@ mod tests {
} }
})) }))
.unwrap(); .unwrap();
assert_eq!(bytes, expected); assert_eq!(bytes, expected);
} }
@ -361,6 +364,7 @@ mod tests {
.uri("/users/me/change-password") .uri("/users/me/change-password")
.method(Method::POST) .method(Method::POST)
.to_request(); .to_request();
let resp = test::call_service(&app, req).await; let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
@ -368,7 +372,10 @@ mod tests {
let body = resp.into_body(); let body = resp.into_body();
let json: serde_json::Value = let json: serde_json::Value =
serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap(); serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap();
assert_eq!(json, serde_json::json!({ "error": "New password cannot be same as old password." })); assert_eq!(
json,
serde_json::json!({ "error": "New password cannot be same as old password." })
);
} }
#[traced_test] #[traced_test]
@ -409,6 +416,7 @@ mod tests {
.uri("/users/me/change-password") .uri("/users/me/change-password")
.method(Method::POST) .method(Method::POST)
.to_request(); .to_request();
let resp = test::call_service(&app, req).await; let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
@ -416,6 +424,9 @@ mod tests {
let body = resp.into_body(); let body = resp.into_body();
let json: serde_json::Value = let json: serde_json::Value =
serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap(); serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap();
assert_eq!(json, serde_json::json!({ "message": "Password updated successfully" })); assert_eq!(
json,
serde_json::json!({ "message": "Password updated successfully" })
);
} }
} }

View File

@ -21,7 +21,7 @@ struct Input {
new_password: String, new_password: String,
} }
#[post("/reset-password/{uidb}/{token}/")] #[post("/reset-password/{uidb}/{token}")]
pub async fn reset_password( pub async fn reset_password(
_: RequireInstanceConfigured, _: RequireInstanceConfigured,
req: HttpRequest, req: HttpRequest,
@ -88,7 +88,7 @@ async fn try_reset_password(
match super::password::validate(&payload.new_password) { match super::password::validate(&payload.new_password) {
PassValidity::Valid => {} PassValidity::Valid => {}
v => { v => {
return Err(JsonError::new("Password is innvalid") return Err(JsonError::new("Password is invalid")
.with_status(StatusCode::BAD_REQUEST) .with_status(StatusCode::BAD_REQUEST)
.with_details(v)) .with_details(v))
} }
@ -110,3 +110,282 @@ async fn try_reset_password(
.await .await
.map_err(Into::into) .map_err(Into::into)
} }
#[cfg(test)]
mod tests {
use actix_jwt_session::{
Hashing, SessionMiddlewareFactory, JWT_COOKIE_NAME, JWT_HEADER_NAME, REFRESH_COOKIE_NAME,
REFRESH_HEADER_NAME,
};
use actix_web::body::to_bytes;
use actix_web::http::header::ContentType;
use actix_web::web::Data;
use actix_web::{test, App};
use jet_contract::deadpool_redis;
use reqwest::{Method, StatusCode};
use sea_orm::Database;
use tracing_test::traced_test;
use uuid::Uuid;
use super::*;
use crate::session;
use crate::utils::{pass_reset_token, uidb};
const PASS_RESET_SECRET: &str = "123ahsdih2he8dagw7";
macro_rules! create_app {
($app: ident, $session_storage: ident, $db: ident) => {
std::env::set_var("DATABASE_URL", "postgres://postgres@0.0.0.0:5432/jet_test");
let redis = deadpool_redis::Config::from_url("redis://0.0.0.0:6379")
.create_pool(Some(deadpool_redis::Runtime::Tokio1))
.expect("Can't connect to redis");
let $db: sea_orm::prelude::DatabaseConnection =
Database::connect("postgres://postgres@0.0.0.0:5432/jet_test")
.await
.expect("Failed to connect to database");
let ($session_storage, factory) =
SessionMiddlewareFactory::<session::AppClaims>::build_ed_dsa()
.with_redis_pool(redis.clone())
// Check if header "Authorization" exists and contains Bearer with encoded JWT
.with_jwt_header(JWT_HEADER_NAME)
// Check if cookie JWT exists and contains encoded JWT
.with_jwt_cookie(JWT_COOKIE_NAME)
.with_refresh_header(REFRESH_HEADER_NAME)
// Check if cookie JWT exists and contains encoded JWT
.with_refresh_cookie(REFRESH_COOKIE_NAME)
.with_jwt_json(&["access_token"])
.finish();
let $db = Data::new($db.clone());
ensure_instance($db.clone()).await;
let $app = test::init_service(
App::new()
.app_data(Data::new($session_storage.clone()))
.app_data($db.clone())
.app_data(Data::new(redis))
.app_data(Data::new(PasswordResetSecret::new(
PASS_RESET_SECRET.to_string(),
)))
.app_data(Data::new(PasswordResetTimeout::new("6d")))
.wrap(actix_web::middleware::NormalizePath::trim())
.wrap(actix_web::middleware::Logger::default())
.wrap(factory)
.service(reset_password),
)
.await;
};
}
async fn ensure_instance(db: Data<DatabaseConnection>) {
use entities::instances::*;
use entities::prelude::Instances;
use sea_orm::*;
if Instances::find().count(&**db).await.unwrap() > 0 {
return;
}
ActiveModel {
instance_name: Set("Plan Free".into()),
is_setup_done: Set(true),
..Default::default()
}
.save(&**db)
.await
.unwrap();
}
async fn create_user(
db: Data<DatabaseConnection>,
user_name: &str,
pass: &str,
) -> entities::users::Model {
use entities::users::*;
use sea_orm::*;
if let Ok(Some(user)) = Users::find()
.filter(Column::Email.eq(format!("{user_name}@example.com")))
.one(&**db)
.await
{
return user;
}
let pass = Hashing::encrypt(pass).unwrap();
Users::insert(ActiveModel {
password: Set(pass),
email: Set(Some(format!("{user_name}@example.com"))),
display_name: Set(user_name.to_string()),
username: Set(Uuid::new_v4().to_string()),
first_name: Set("".to_string()),
last_name: Set("".to_string()),
last_location: Set("".to_string()),
created_location: Set("".to_string()),
is_password_autoset: Set(false),
token: Set(Uuid::new_v4().to_string()),
billing_address_country: Set("".to_string()),
user_timezone: Set("UTC".to_string()),
last_login_ip: Set("0.0.0.0".to_string()),
last_login_medium: Set("None".to_string()),
last_logout_ip: Set("0.0.0.0".to_string()),
last_login_uagent: Set("test".to_string()),
is_active: Set(true),
avatar: Set("".to_string()),
..Default::default()
})
.exec_with_returning(&**db)
.await
.unwrap()
}
#[traced_test]
#[tokio::test]
async fn path_exists() {
create_app!(app, session, db);
let req = test::TestRequest::default()
.insert_header(ContentType::json())
.uri("/reset-password/uidb/token")
.method(Method::POST)
.to_request();
let resp = test::call_service(&app, req).await;
assert_ne!(resp.status(), StatusCode::NOT_FOUND);
}
#[traced_test]
#[tokio::test]
async fn invalid_new_pass() {
create_app!(app, session, db);
let user = create_user(
db.clone(),
"invalid_new_pass_reset_pass",
"ASDFasdf1234!@#$",
)
.await;
let uidb = uidb::encode(user.id);
let token = pass_reset_token::make_token(&user, PASS_RESET_SECRET);
let req = test::TestRequest::default()
.insert_header(ContentType::json())
.uri(&format!("/reset-password/{uidb}/{token}"))
.method(Method::POST)
.set_json(serde_json::json!({ "new_password": "d)" }))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body = resp.into_body();
let json: serde_json::Value =
serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap();
assert_eq!(
json,
serde_json::json!({
"error": "Password is invalid",
"errors": {
"has_lower": true,
"has_num": false,
"has_special": true,
"has_upper": false,
"too_long": false,
"too_short": true
}
})
);
}
#[traced_test]
#[tokio::test]
async fn invalid_uidb() {
create_app!(app, session, db);
let user = create_user(db.clone(), "invalid_uidb_reset_pass", "ASDFasdf1234!@#$").await;
let uidb = "aiodsjhoahsd9ays9d8";
let token = pass_reset_token::make_token(&user, PASS_RESET_SECRET);
let req = test::TestRequest::default()
.insert_header(ContentType::json())
.uri(&format!("/reset-password/{uidb}/{token}"))
.method(Method::POST)
.set_json(serde_json::json!({ "new_password": "LKJpoi098)(*" }))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
let body = resp.into_body();
let json: serde_json::Value =
serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap();
assert_eq!(
json,
serde_json::json!({
"error": "Token is invalid"
})
);
}
#[traced_test]
#[tokio::test]
async fn invalid_token() {
create_app!(app, session, db);
let user = create_user(db.clone(), "invalid_token_reset_pass", "ASDFasdf1234!@#$").await;
let uidb = uidb::encode(user.id);
let token = "aiosjd9ahsd98hasa";
let req = test::TestRequest::default()
.insert_header(ContentType::json())
.uri(&format!("/reset-password/{uidb}/{token}"))
.method(Method::POST)
.set_json(serde_json::json!({ "new_password": "LKJpoi098)(*" }))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body = resp.into_body();
let json: serde_json::Value =
serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap();
assert_eq!(
json,
serde_json::json!({
"error": "Token is invalid"
})
);
}
#[traced_test]
#[tokio::test]
async fn valid() {
create_app!(app, session, db);
let user = create_user(db.clone(), "valid_reset_pass", "ASDFasdf1234!@#$").await;
let uidb = uidb::encode(user.id);
let token = pass_reset_token::make_token(&user, PASS_RESET_SECRET);
let req = test::TestRequest::default()
.insert_header(ContentType::json())
.uri(&format!("/reset-password/{uidb}/{token}"))
.method(Method::POST)
.set_json(serde_json::json!({ "new_password": "LKJpoi098)(*" }))
.to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = resp.into_body();
let json: serde_json::Value =
serde_json::from_slice(&to_bytes(body).await.unwrap()[..]).unwrap();
let keys = json.as_object().unwrap().keys().collect::<Vec<_>>();
assert_eq!(
keys,
vec![&"access_token".to_string(), &"refresh_token".to_string()]
);
}
}

View File

@ -294,12 +294,39 @@ pub async fn invites_to_membership(
} }
pub mod uidb { pub mod uidb {
use actix_web::{FromRequest, ResponseError};
use base64::prelude::*; use base64::prelude::*;
use derive_more::{Constructor, Deref};
use futures_util::future::LocalBoxFuture;
use futures_util::FutureExt;
use reqwest::StatusCode; use reqwest::StatusCode;
use uuid::Uuid; use uuid::Uuid;
use crate::models::JsonError; use crate::models::JsonError;
#[derive(Debug, thiserror::Error)]
#[error("Unauthorized")]
pub struct Unauthorized;
impl ResponseError for Unauthorized {}
#[derive(Debug, PartialEq, Deref, Constructor)]
pub struct Uidb(String);
impl FromRequest for Uidb {
type Error = Unauthorized;
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
fn from_request(
req: &actix_web::HttpRequest,
_payload: &mut actix_web::dev::Payload,
) -> Self::Future {
eprintln!("match info {:?}", req.match_info());
eprintln!("as str {:?}", req.match_info().as_str());
async move { Err(Unauthorized) }.boxed_local()
}
}
pub fn decode(uidb: &str) -> Result<Uuid, JsonError> { pub fn decode(uidb: &str) -> Result<Uuid, JsonError> {
let Ok(bytes) = BASE64_URL_SAFE.decode(uidb) else { let Ok(bytes) = BASE64_URL_SAFE.decode(uidb) else {
return Err(JsonError::new("Token is invalid").with_status(StatusCode::UNAUTHORIZED)); return Err(JsonError::new("Token is invalid").with_status(StatusCode::UNAUTHORIZED));
@ -350,7 +377,8 @@ pub mod pass_reset_token {
Hmac::<Sha256>::new_from_slice(secret.as_bytes()).expect("Invalid hmac secret"); Hmac::<Sha256>::new_from_slice(secret.as_bytes()).expect("Invalid hmac secret");
mac.update(hash_value.as_bytes()); mac.update(hash_value.as_bytes());
let result = mac.finalize(); let result = mac.finalize();
let s = String::from_utf8(result.into_bytes()[..].to_vec()).unwrap(); let bytes = &result.into_bytes()[..];
let s = bytes.iter().map(|b| format!("{b:02x}")).collect::<String>();
format!("{ts_b36}-{s}") format!("{ts_b36}-{s}")
} }