feat: add login test
This commit is contained in:
parent
508961d07d
commit
2429a41b2f
|
|
@ -1,19 +1,58 @@
|
|||
{
|
||||
"db": "PostgreSQL",
|
||||
"e8c487b4314c267f6da2667b95f6c8003fabc2461c10df2d6d39d081e74e167f": {
|
||||
"query": "\n INSERT INTO user (uid, username, password, email,create_time)\n VALUES ($1, $2, $3, $4, $5)\n ",
|
||||
"1775f308cda63b61c0aaf4d8d1c7cf00f6bff5041e821575f65867c32d42f1b0": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz"
|
||||
"Timestamptz",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO users (uid, email, username, create_time, password)\n VALUES ($1, $2, $3, $4, $5)\n "
|
||||
},
|
||||
"b188bb8915fa0fc8c5dfefa27f1b086b839633c3c4dc6e4991b43a608d46a170": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "uid",
|
||||
"ordinal": 0,
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT uid, password\n FROM users\n WHERE email = $1\n "
|
||||
},
|
||||
"b754835f0a1543fc32acbfee436f775e542036888e7a2051622efa435debb318": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE users\n SET password= $1\n WHERE uid = $2\n "
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
use crate::component::auth::{
|
||||
register_user, InputParamsError, LoginRequest, RegisterRequestParams,
|
||||
login, register, InputParamsError, LoginRequest, RegisterRequestParams,
|
||||
};
|
||||
use crate::domain::{UserEmail, UserName, UserPassword};
|
||||
use crate::state::State;
|
||||
|
|
@ -10,21 +10,36 @@ use actix_web::{web, HttpResponse, Scope};
|
|||
|
||||
pub fn user_scope() -> Scope {
|
||||
web::scope("/api/user")
|
||||
.service(web::resource("/login").route(web::post().to(login)))
|
||||
.service(web::resource("/logout").route(web::get().to(logout)))
|
||||
.service(web::resource("/register").route(web::post().to(register)))
|
||||
.service(web::resource("/login").route(web::post().to(login_handler)))
|
||||
.service(web::resource("/logout").route(web::get().to(logout_handler)))
|
||||
.service(web::resource("/register").route(web::post().to(register_handler)))
|
||||
}
|
||||
|
||||
async fn login(id: Identity, req: Data<LoginRequest>, state: Data<State>) -> Result<HttpResponse> {
|
||||
todo!()
|
||||
async fn login_handler(req: Json<LoginRequest>, state: Data<State>) -> Result<HttpResponse> {
|
||||
let params = req.into_inner();
|
||||
let email = UserEmail::parse(params.email)
|
||||
.map_err(|e| InputParamsError::InvalidEmail(e))?
|
||||
.0;
|
||||
let password = UserPassword::parse(params.password)
|
||||
.map_err(|_| InputParamsError::InvalidPassword)?
|
||||
.0;
|
||||
let resp = login(state.pg_pool.clone(), state.cache.clone(), email, password).await?;
|
||||
Ok(HttpResponse::Ok().json(resp))
|
||||
}
|
||||
|
||||
async fn logout(payload: Payload, id: Identity, state: Data<State>) -> Result<HttpResponse> {
|
||||
async fn logout_handler(
|
||||
payload: Payload,
|
||||
id: Identity,
|
||||
state: Data<State>,
|
||||
) -> Result<HttpResponse> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(state))]
|
||||
async fn register(req: Json<RegisterRequestParams>, state: Data<State>) -> Result<HttpResponse> {
|
||||
async fn register_handler(
|
||||
req: Json<RegisterRequestParams>,
|
||||
state: Data<State>,
|
||||
) -> Result<HttpResponse> {
|
||||
let params = req.into_inner();
|
||||
let name = UserName::parse(params.name)
|
||||
.map_err(|e| InputParamsError::InvalidName(e))?
|
||||
|
|
@ -36,7 +51,7 @@ async fn register(req: Json<RegisterRequestParams>, state: Data<State>) -> Resul
|
|||
.map_err(|_| InputParamsError::InvalidPassword)?
|
||||
.0;
|
||||
|
||||
let resp = register_user(
|
||||
let resp = register(
|
||||
state.pg_pool.clone(),
|
||||
state.cache.clone(),
|
||||
name,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use crate::component::auth::AuthError;
|
||||
use crate::telemetry::spawn_blocking_with_tracing;
|
||||
use anyhow::Context;
|
||||
use argon2::password_hash::SaltString;
|
||||
|
|
@ -6,7 +7,7 @@ use secrecy::{ExposeSecret, Secret};
|
|||
use sqlx::PgPool;
|
||||
|
||||
pub struct Credentials {
|
||||
pub username: String,
|
||||
pub email: String,
|
||||
pub password: Secret<String>,
|
||||
}
|
||||
|
||||
|
|
@ -14,8 +15,8 @@ pub struct Credentials {
|
|||
pub async fn validate_credentials(
|
||||
credentials: Credentials,
|
||||
pool: &PgPool,
|
||||
) -> Result<uuid::Uuid, anyhow::Error> {
|
||||
let mut user_id = None;
|
||||
) -> Result<uuid::Uuid, AuthError> {
|
||||
let mut uid = None;
|
||||
let mut expected_hash_password = Secret::new(
|
||||
"$argon2id$v=19$m=15000,t=2,p=1$\
|
||||
gZiV/M1gPc22ElAH/Jh1Hw$\
|
||||
|
|
@ -23,10 +24,10 @@ pub async fn validate_credentials(
|
|||
.to_string(),
|
||||
);
|
||||
|
||||
if let Some((stored_user_id, stored_hash_password)) =
|
||||
get_stored_credentials(&credentials.username, pool).await?
|
||||
if let Some((stored_uid, stored_hash_password)) =
|
||||
get_stored_credentials(&credentials.email, pool).await?
|
||||
{
|
||||
user_id = Some(stored_user_id);
|
||||
uid = Some(stored_uid);
|
||||
expected_hash_password = stored_hash_password;
|
||||
}
|
||||
|
||||
|
|
@ -36,7 +37,8 @@ pub async fn validate_credentials(
|
|||
.await
|
||||
.context("Failed to spawn blocking task.")??;
|
||||
|
||||
user_id.ok_or_else(|| anyhow::anyhow!("Unknown username."))
|
||||
uid.ok_or_else(|| anyhow::anyhow!("Unknown email."))
|
||||
.map_err(AuthError::InvalidCredentials)
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "Change password", skip(password, pool))]
|
||||
|
|
|
|||
|
|
@ -1,20 +1,56 @@
|
|||
use crate::component::auth::{compute_hash_password, internal_error, AuthError};
|
||||
use crate::component::auth::{
|
||||
compute_hash_password, internal_error, validate_credentials, AuthError, Credentials,
|
||||
};
|
||||
use crate::config::env::{domain, jwt_secret};
|
||||
use crate::state::Cache;
|
||||
use actix_web::http::header::HeaderValue;
|
||||
use actix_web::{FromRequest, HttpRequest};
|
||||
use anyhow::Context;
|
||||
use anyhow::{Context, Error};
|
||||
use chrono::Utc;
|
||||
use chrono::{Duration, Local};
|
||||
use futures_util::future::{ready, Ready};
|
||||
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
|
||||
use secrecy::Secret;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::types::uuid;
|
||||
use sqlx::{PgPool, Postgres, Transaction};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn register_user(
|
||||
pub async fn login(
|
||||
pg_pool: PgPool,
|
||||
cache: Arc<RwLock<Cache>>,
|
||||
email: String,
|
||||
password: String,
|
||||
) -> Result<LoginResponse, AuthError> {
|
||||
let credentials = Credentials {
|
||||
email,
|
||||
password: Secret::new(password),
|
||||
};
|
||||
|
||||
match validate_credentials(credentials, &pg_pool).await? {
|
||||
Ok(uid) => {
|
||||
let uid = uid.to_string();
|
||||
let token = Token::create_token(&uid)?.into();
|
||||
let logged_user = LoggedUser::new(uid);
|
||||
cache.write().await.authorized(logged_user);
|
||||
|
||||
Ok(LoginResponse {
|
||||
token,
|
||||
user_id: uid,
|
||||
})
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn logout(logged_user: LoggedUser, cache: Arc<RwLock<Cache>>) -> Result<(), AuthError> {
|
||||
cache.write().await.unauthorized(logged_user);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn register(
|
||||
pg_pool: PgPool,
|
||||
cache: Arc<RwLock<Cache>>,
|
||||
username: String,
|
||||
|
|
@ -87,6 +123,12 @@ pub struct LoginRequest {
|
|||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug)]
|
||||
pub struct LoginResponse {
|
||||
pub token: String,
|
||||
pub user_id: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, Debug)]
|
||||
pub struct RegisterRequestParams {
|
||||
pub email: String,
|
||||
|
|
@ -101,20 +143,18 @@ pub struct RegisterResponse {
|
|||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Ord, PartialOrd)]
|
||||
pub struct LoggedUser {
|
||||
pub user_id: String,
|
||||
pub uid: String,
|
||||
}
|
||||
|
||||
impl std::convert::From<Claim> for LoggedUser {
|
||||
fn from(c: Claim) -> Self {
|
||||
Self {
|
||||
user_id: c.user_id(),
|
||||
}
|
||||
Self { uid: c.user_id() }
|
||||
}
|
||||
}
|
||||
|
||||
impl LoggedUser {
|
||||
pub fn new(user_id: String) -> Self {
|
||||
Self { user_id }
|
||||
Self { uid: user_id }
|
||||
}
|
||||
|
||||
pub fn from_token(token: String) -> Result<Self, AuthError> {
|
||||
|
|
@ -123,7 +163,7 @@ impl LoggedUser {
|
|||
}
|
||||
|
||||
pub fn as_uuid(&self) -> Result<uuid::Uuid, anyhow::Error> {
|
||||
let uuid = uuid::Uuid::parse_str(&self.user_id).context("Invalid uuid")?;
|
||||
let uuid = uuid::Uuid::parse_str(&self.uid).context("Invalid uuid")?;
|
||||
Ok(uuid)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::test_server::spawn_server;
|
||||
use appflowy_server::component::auth::RegisterResponse;
|
||||
use reqwest::StatusCode;
|
||||
|
||||
#[tokio::test]
|
||||
// curl -X POST --url http://0.0.0.0:8000/api/user/register --header 'content-type: application/json' --data '{"name":"fake name", "email":"fake@appflowy.io", "password":"Fake@123"}'
|
||||
|
|
@ -14,3 +15,28 @@ async fn register_success() {
|
|||
|
||||
println!("{:?}", response);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_with_invalid_password() {
|
||||
let server = spawn_server().await;
|
||||
let http_resp = server.register("user 1", "fake@appflowy.io", "123").await;
|
||||
assert_eq!(http_resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_with_invalid_name() {
|
||||
let server = spawn_server().await;
|
||||
let http_resp = server
|
||||
.register("", "fake@appflowy.io", "FakePassword!123")
|
||||
.await;
|
||||
assert_eq!(http_resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn register_with_invalid_email() {
|
||||
let server = spawn_server().await;
|
||||
let http_resp = server
|
||||
.register("me", "appflowy.io", "FakePassword!123")
|
||||
.await;
|
||||
assert_eq!(http_resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,13 @@ use sqlx::{Connection, Executor, PgConnection, PgPool};
|
|||
|
||||
// Ensure that the `tracing` stack is only initialised once using `once_cell`
|
||||
static TRACING: Lazy<()> = Lazy::new(|| {
|
||||
let default_filter_level = "info".to_string();
|
||||
let level = "trace".to_string();
|
||||
let mut filters = vec![];
|
||||
filters.push(format!("appflowy_server={}", level));
|
||||
filters.push(format!("hyper={}", level));
|
||||
|
||||
let subscriber_name = "test".to_string();
|
||||
let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::stdout);
|
||||
let subscriber = get_subscriber(subscriber_name, filters.join(","), std::io::stdout);
|
||||
init_subscriber(subscriber);
|
||||
});
|
||||
|
||||
|
|
@ -42,7 +46,6 @@ impl TestServer {
|
|||
|
||||
pub async fn spawn_server() -> TestServer {
|
||||
Lazy::force(&TRACING);
|
||||
|
||||
let database_name = Uuid::new_v4().to_string();
|
||||
let config = {
|
||||
let mut config = get_configuration().expect("Failed to read configuration.");
|
||||
|
|
@ -58,18 +61,18 @@ pub async fn spawn_server() -> TestServer {
|
|||
.await
|
||||
.expect("Failed to build application.");
|
||||
|
||||
let port = application.port();
|
||||
let address = format!("http://localhost:{}", port);
|
||||
let _ = tokio::spawn(async {
|
||||
let _ = application.run_until_stopped().await;
|
||||
});
|
||||
|
||||
let api_client = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.cookie_store(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let port = application.port();
|
||||
let address = format!("http://localhost:{}", port);
|
||||
let _ = tokio::spawn(async {
|
||||
let _ = application.run_until_stopped();
|
||||
});
|
||||
|
||||
TestServer {
|
||||
state,
|
||||
api_client,
|
||||
|
|
@ -100,3 +103,31 @@ async fn configure_database(config: &DatabaseSetting) -> PgPool {
|
|||
|
||||
connection_pool
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct TestUser {
|
||||
name: String,
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl TestUser {
|
||||
pub fn generate() -> Self {
|
||||
Self {
|
||||
name: "Me".to_string(),
|
||||
email: "me@appflowy.io".to_string(),
|
||||
password: "HelloAppFlowy123".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register(&self, test_server: &TestServer) {
|
||||
let url = format!("{}/api/user/register", test_server.address);
|
||||
test_server
|
||||
.api_client
|
||||
.post(&url)
|
||||
.json(&self)
|
||||
.send()
|
||||
.await
|
||||
.expect("Fail to register user");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue