test reset password
This commit is contained in:
parent
09d164576f
commit
60c9c915e7
@ -220,6 +220,7 @@ mod tests {
|
||||
let req = test::TestRequest::default()
|
||||
.insert_header(ContentType::json())
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert!(resp.status().is_client_error());
|
||||
@ -257,6 +258,7 @@ mod tests {
|
||||
.uri("/users/me/change-password")
|
||||
.method(Method::POST)
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
@ -320,6 +322,7 @@ mod tests {
|
||||
}
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(bytes, expected);
|
||||
}
|
||||
|
||||
@ -361,6 +364,7 @@ mod tests {
|
||||
.uri("/users/me/change-password")
|
||||
.method(Method::POST)
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
@ -368,7 +372,10 @@ mod tests {
|
||||
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": "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]
|
||||
@ -409,6 +416,7 @@ mod tests {
|
||||
.uri("/users/me/change-password")
|
||||
.method(Method::POST)
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&app, req).await;
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
@ -416,6 +424,9 @@ mod tests {
|
||||
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!({ "message": "Password updated successfully" }));
|
||||
assert_eq!(
|
||||
json,
|
||||
serde_json::json!({ "message": "Password updated successfully" })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ struct Input {
|
||||
new_password: String,
|
||||
}
|
||||
|
||||
#[post("/reset-password/{uidb}/{token}/")]
|
||||
#[post("/reset-password/{uidb}/{token}")]
|
||||
pub async fn reset_password(
|
||||
_: RequireInstanceConfigured,
|
||||
req: HttpRequest,
|
||||
@ -88,7 +88,7 @@ async fn try_reset_password(
|
||||
match super::password::validate(&payload.new_password) {
|
||||
PassValidity::Valid => {}
|
||||
v => {
|
||||
return Err(JsonError::new("Password is innvalid")
|
||||
return Err(JsonError::new("Password is invalid")
|
||||
.with_status(StatusCode::BAD_REQUEST)
|
||||
.with_details(v))
|
||||
}
|
||||
@ -110,3 +110,282 @@ async fn try_reset_password(
|
||||
.await
|
||||
.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 {
|
||||
use actix_web::{FromRequest, ResponseError};
|
||||
use base64::prelude::*;
|
||||
use derive_more::{Constructor, Deref};
|
||||
use futures_util::future::LocalBoxFuture;
|
||||
use futures_util::FutureExt;
|
||||
use reqwest::StatusCode;
|
||||
use uuid::Uuid;
|
||||
|
||||
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> {
|
||||
let Ok(bytes) = BASE64_URL_SAFE.decode(uidb) else {
|
||||
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");
|
||||
mac.update(hash_value.as_bytes());
|
||||
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}")
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user