test reset password
This commit is contained in:
parent
09d164576f
commit
60c9c915e7
@ -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" })
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user