feat: add login test

This commit is contained in:
nathan 2023-03-12 19:31:55 +08:00
parent 508961d07d
commit 2429a41b2f
6 changed files with 194 additions and 41 deletions

View File

@ -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 "
}
}

View File

@ -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,

View File

@ -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))]

View File

@ -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)
}
}

View File

@ -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);
}

View File

@ -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");
}
}