feat: auth and account management API
This commit is contained in:
parent
7185bac60b
commit
04d7f872d5
11 changed files with 1100 additions and 19 deletions
4
.github/workflows/linux.yml
vendored
4
.github/workflows/linux.yml
vendored
|
@ -83,11 +83,11 @@ jobs:
|
||||||
run: make docker
|
run: make docker
|
||||||
|
|
||||||
- name: publish docker images
|
- name: publish docker images
|
||||||
if: (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'realaravinth/librepages'
|
if: (github.ref == 'refs/heads/master' && github.event_name == 'push') && github.repository == 'realaravinth/librepages'
|
||||||
run: make docker-publish
|
run: make docker-publish
|
||||||
|
|
||||||
- name: publish bins
|
- name: publish bins
|
||||||
if: (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'realaravinth/librepages'
|
if: (github.ref == 'refs/heads/master' && github.event_name == 'push') && github.repository == 'realaravinth/librepages'
|
||||||
run: ./scripts/bin-publish.sh publish master latest $DUMBSERVE_PASSWORD
|
run: ./scripts/bin-publish.sh publish master latest $DUMBSERVE_PASSWORD
|
||||||
env:
|
env:
|
||||||
DUMBSERVE_PASSWORD: ${{ secrets.DUMBSERVE_PASSWORD }}
|
DUMBSERVE_PASSWORD: ${{ secrets.DUMBSERVE_PASSWORD }}
|
||||||
|
|
17
src/api/mod.rs
Normal file
17
src/api/mod.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
pub mod v1;
|
138
src/api/v1/account/mod.rs
Normal file
138
src/api/v1/account/mod.rs
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::ctx::api::v1::account::*;
|
||||||
|
use crate::ctx::api::v1::auth::Password;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::AppCtx;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod test;
|
||||||
|
|
||||||
|
pub use super::auth;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct AccountCheckPayload {
|
||||||
|
pub val: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
|
||||||
|
cfg.service(username_exists);
|
||||||
|
cfg.service(set_username);
|
||||||
|
cfg.service(email_exists);
|
||||||
|
cfg.service(set_email);
|
||||||
|
cfg.service(delete_account);
|
||||||
|
cfg.service(update_user_password);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Email {
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct Username {
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// update username
|
||||||
|
#[actix_web_codegen_const_routes::post(
|
||||||
|
path = "crate::V1_API_ROUTES.account.update_username",
|
||||||
|
wrap = "super::get_auth_middleware()"
|
||||||
|
)]
|
||||||
|
async fn set_username(
|
||||||
|
id: Identity,
|
||||||
|
payload: web::Json<Username>,
|
||||||
|
ctx: AppCtx,
|
||||||
|
) -> ServiceResult<impl Responder> {
|
||||||
|
let username = id.identity().unwrap();
|
||||||
|
|
||||||
|
let new_name = ctx.update_username(&username, &payload.username).await?;
|
||||||
|
|
||||||
|
id.forget();
|
||||||
|
id.remember(new_name);
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.account.username_exists")]
|
||||||
|
async fn username_exists(
|
||||||
|
payload: web::Json<AccountCheckPayload>,
|
||||||
|
ctx: AppCtx,
|
||||||
|
) -> ServiceResult<impl Responder> {
|
||||||
|
Ok(HttpResponse::Ok().json(ctx.username_exists(&payload.val).await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.account.email_exists")]
|
||||||
|
pub async fn email_exists(
|
||||||
|
payload: web::Json<AccountCheckPayload>,
|
||||||
|
ctx: AppCtx,
|
||||||
|
) -> ServiceResult<impl Responder> {
|
||||||
|
Ok(HttpResponse::Ok().json(ctx.email_exists(&payload.val).await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// update email
|
||||||
|
#[actix_web_codegen_const_routes::post(
|
||||||
|
path = "crate::V1_API_ROUTES.account.update_email",
|
||||||
|
wrap = "super::get_auth_middleware()"
|
||||||
|
)]
|
||||||
|
async fn set_email(
|
||||||
|
id: Identity,
|
||||||
|
payload: web::Json<Email>,
|
||||||
|
ctx: AppCtx,
|
||||||
|
) -> ServiceResult<impl Responder> {
|
||||||
|
let username = id.identity().unwrap();
|
||||||
|
ctx.set_email(&username, &payload.email).await?;
|
||||||
|
Ok(HttpResponse::Ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web_codegen_const_routes::post(
|
||||||
|
path = "crate::V1_API_ROUTES.account.delete",
|
||||||
|
wrap = "super::get_auth_middleware()"
|
||||||
|
)]
|
||||||
|
async fn delete_account(
|
||||||
|
id: Identity,
|
||||||
|
payload: web::Json<Password>,
|
||||||
|
ctx: AppCtx,
|
||||||
|
) -> ServiceResult<impl Responder> {
|
||||||
|
let username = id.identity().unwrap();
|
||||||
|
|
||||||
|
ctx.delete_user(&username, &payload.password).await?;
|
||||||
|
id.forget();
|
||||||
|
Ok(HttpResponse::Ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web_codegen_const_routes::post(
|
||||||
|
path = "crate::V1_API_ROUTES.account.update_password",
|
||||||
|
wrap = "super::get_auth_middleware()"
|
||||||
|
)]
|
||||||
|
async fn update_user_password(
|
||||||
|
id: Identity,
|
||||||
|
ctx: AppCtx,
|
||||||
|
|
||||||
|
payload: web::Json<ChangePasswordReqest>,
|
||||||
|
) -> ServiceResult<impl Responder> {
|
||||||
|
let username = id.identity().unwrap();
|
||||||
|
let payload = payload.into_inner();
|
||||||
|
ctx.change_password(&username, &payload).await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok())
|
||||||
|
}
|
296
src/api/v1/account/test.rs
Normal file
296
src/api/v1/account/test.rs
Normal file
|
@ -0,0 +1,296 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::test;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::api::v1::ROUTES;
|
||||||
|
use crate::ctx::api::v1::auth::Password;
|
||||||
|
use crate::ctx::Ctx;
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn postgrest_account_works() {
|
||||||
|
let (_, ctx) = crate::tests::get_ctx().await;
|
||||||
|
uname_email_exists_works(ctx.clone()).await;
|
||||||
|
email_udpate_password_validation_del_userworks(ctx.clone()).await;
|
||||||
|
username_update_works(ctx.clone()).await;
|
||||||
|
update_password_works(ctx.clone()).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn uname_email_exists_works(ctx: Arc<Ctx>) {
|
||||||
|
const NAME: &str = "testuserexists";
|
||||||
|
const PASSWORD: &str = "longpasswordasdfa2";
|
||||||
|
const EMAIL: &str = "testuserexists2@a.com";
|
||||||
|
|
||||||
|
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||||
|
|
||||||
|
let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||||
|
let cookies = get_cookie!(signin_resp);
|
||||||
|
let app = get_app!(ctx).await;
|
||||||
|
|
||||||
|
let mut payload = AccountCheckPayload { val: NAME.into() };
|
||||||
|
|
||||||
|
let user_exists_resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&payload, ROUTES.account.username_exists)
|
||||||
|
.cookie(cookies.clone())
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(user_exists_resp.status(), StatusCode::OK);
|
||||||
|
let mut resp: AccountCheckResp = test::read_body_json(user_exists_resp).await;
|
||||||
|
assert!(resp.exists);
|
||||||
|
|
||||||
|
payload.val = PASSWORD.into();
|
||||||
|
|
||||||
|
let user_doesnt_exist = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&payload, ROUTES.account.username_exists)
|
||||||
|
.cookie(cookies.clone())
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(user_doesnt_exist.status(), StatusCode::OK);
|
||||||
|
resp = test::read_body_json(user_doesnt_exist).await;
|
||||||
|
assert!(!resp.exists);
|
||||||
|
|
||||||
|
let email_doesnt_exist = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&payload, ROUTES.account.email_exists)
|
||||||
|
.cookie(cookies.clone())
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(email_doesnt_exist.status(), StatusCode::OK);
|
||||||
|
resp = test::read_body_json(email_doesnt_exist).await;
|
||||||
|
assert!(!resp.exists);
|
||||||
|
|
||||||
|
payload.val = EMAIL.into();
|
||||||
|
|
||||||
|
let email_exist = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&payload, ROUTES.account.email_exists)
|
||||||
|
.cookie(cookies.clone())
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(email_exist.status(), StatusCode::OK);
|
||||||
|
resp = test::read_body_json(email_exist).await;
|
||||||
|
assert!(resp.exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn email_udpate_password_validation_del_userworks(ctx: Arc<Ctx>) {
|
||||||
|
const NAME: &str = "testuser2";
|
||||||
|
const PASSWORD: &str = "longpassword2";
|
||||||
|
const EMAIL: &str = "testuser1@a.com2";
|
||||||
|
const NAME2: &str = "eupdauser";
|
||||||
|
const EMAIL2: &str = "eupdauser@a.com";
|
||||||
|
|
||||||
|
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||||
|
let _ = ctx.delete_user(NAME2, PASSWORD).await;
|
||||||
|
|
||||||
|
let _ = ctx.register_and_signin(NAME2, EMAIL2, PASSWORD).await;
|
||||||
|
let (_creds, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||||
|
let cookies = get_cookie!(signin_resp);
|
||||||
|
let app = get_app!(ctx).await;
|
||||||
|
|
||||||
|
// update email
|
||||||
|
let mut email_payload = Email {
|
||||||
|
email: EMAIL.into(),
|
||||||
|
};
|
||||||
|
let email_update_resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&email_payload, ROUTES.account.update_email)
|
||||||
|
//post_request!(&email_payload, EMAIL_UPDATE)
|
||||||
|
.cookie(cookies.clone())
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(email_update_resp.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
// check duplicate email while duplicate email
|
||||||
|
email_payload.email = EMAIL2.into();
|
||||||
|
ctx.bad_post_req_test(
|
||||||
|
NAME,
|
||||||
|
PASSWORD,
|
||||||
|
ROUTES.account.update_email,
|
||||||
|
&email_payload,
|
||||||
|
ServiceError::EmailTaken,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// wrong password while deleteing account
|
||||||
|
let mut payload = Password {
|
||||||
|
password: NAME.into(),
|
||||||
|
};
|
||||||
|
ctx.bad_post_req_test(
|
||||||
|
NAME,
|
||||||
|
PASSWORD,
|
||||||
|
ROUTES.account.delete,
|
||||||
|
&payload,
|
||||||
|
ServiceError::WrongPassword,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// delete account
|
||||||
|
payload.password = PASSWORD.into();
|
||||||
|
let delete_user_resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&payload, ROUTES.account.delete)
|
||||||
|
.cookie(cookies.clone())
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(delete_user_resp.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
// try to delete an account that doesn't exist
|
||||||
|
let account_not_found_resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&payload, ROUTES.account.delete)
|
||||||
|
.cookie(cookies)
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(account_not_found_resp.status(), StatusCode::NOT_FOUND);
|
||||||
|
let txt: ErrorToResponse = test::read_body_json(account_not_found_resp).await;
|
||||||
|
assert_eq!(txt.error, format!("{}", ServiceError::AccountNotFound));
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn username_update_works(ctx: Arc<Ctx>) {
|
||||||
|
const NAME: &str = "testuserupda";
|
||||||
|
const EMAIL: &str = "testuserupda@sss.com";
|
||||||
|
const EMAIL2: &str = "testuserupda2@sss.com";
|
||||||
|
const PASSWORD: &str = "longpassword2";
|
||||||
|
const NAME2: &str = "terstusrtds";
|
||||||
|
const NAME_CHANGE: &str = "terstusrtdsxx";
|
||||||
|
|
||||||
|
let _ = futures::join!(
|
||||||
|
ctx.delete_user(NAME, PASSWORD),
|
||||||
|
ctx.delete_user(NAME2, PASSWORD),
|
||||||
|
ctx.delete_user(NAME_CHANGE, PASSWORD)
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = ctx.register_and_signin(NAME2, EMAIL2, PASSWORD).await;
|
||||||
|
let (_creds, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||||
|
let cookies = get_cookie!(signin_resp);
|
||||||
|
let app = get_app!(ctx).await;
|
||||||
|
|
||||||
|
// update username
|
||||||
|
let mut username_udpate = Username {
|
||||||
|
username: NAME_CHANGE.into(),
|
||||||
|
};
|
||||||
|
let username_update_resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&username_udpate, ROUTES.account.update_username)
|
||||||
|
.cookie(cookies)
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(username_update_resp.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
// check duplicate username with duplicate username
|
||||||
|
username_udpate.username = NAME2.into();
|
||||||
|
ctx.bad_post_req_test(
|
||||||
|
NAME_CHANGE,
|
||||||
|
PASSWORD,
|
||||||
|
ROUTES.account.update_username,
|
||||||
|
&username_udpate,
|
||||||
|
ServiceError::UsernameTaken,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_password_works(ctx: Arc<Ctx>) {
|
||||||
|
const NAME: &str = "updatepassuser";
|
||||||
|
const PASSWORD: &str = "longpassword2";
|
||||||
|
const EMAIL: &str = "updatepassuser@a.com";
|
||||||
|
|
||||||
|
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||||
|
|
||||||
|
let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||||
|
let cookies = get_cookie!(signin_resp);
|
||||||
|
let app = get_app!(ctx).await;
|
||||||
|
|
||||||
|
let new_password = "newpassword";
|
||||||
|
|
||||||
|
let update_password = ChangePasswordReqest {
|
||||||
|
password: PASSWORD.into(),
|
||||||
|
new_password: new_password.into(),
|
||||||
|
confirm_new_password: PASSWORD.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = ctx.change_password(NAME, &update_password).await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
assert_eq!(res, Err(ServiceError::PasswordsDontMatch));
|
||||||
|
|
||||||
|
let update_password = ChangePasswordReqest {
|
||||||
|
password: PASSWORD.into(),
|
||||||
|
new_password: new_password.into(),
|
||||||
|
confirm_new_password: new_password.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(ctx.change_password(NAME, &update_password).await.is_ok());
|
||||||
|
|
||||||
|
let update_password = ChangePasswordReqest {
|
||||||
|
password: new_password.into(),
|
||||||
|
new_password: new_password.into(),
|
||||||
|
confirm_new_password: PASSWORD.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.bad_post_req_test(
|
||||||
|
NAME,
|
||||||
|
new_password,
|
||||||
|
ROUTES.account.update_password,
|
||||||
|
&update_password,
|
||||||
|
ServiceError::PasswordsDontMatch,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let update_password = ChangePasswordReqest {
|
||||||
|
password: PASSWORD.into(),
|
||||||
|
new_password: PASSWORD.into(),
|
||||||
|
confirm_new_password: PASSWORD.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.bad_post_req_test(
|
||||||
|
NAME,
|
||||||
|
new_password,
|
||||||
|
ROUTES.account.update_password,
|
||||||
|
&update_password,
|
||||||
|
ServiceError::WrongPassword,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let update_password = ChangePasswordReqest {
|
||||||
|
password: new_password.into(),
|
||||||
|
new_password: PASSWORD.into(),
|
||||||
|
confirm_new_password: PASSWORD.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let update_password_resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&update_password, ROUTES.account.update_password)
|
||||||
|
.cookie(cookies)
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(update_password_resp.status(), StatusCode::OK);
|
||||||
|
}
|
71
src/api/v1/auth.rs
Normal file
71
src/api/v1/auth.rs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::ctx::api::v1::auth::{Login, Register};
|
||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_web::http::header;
|
||||||
|
use actix_web::{web, HttpResponse, Responder};
|
||||||
|
|
||||||
|
use super::RedirectQuery;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::AppCtx;
|
||||||
|
|
||||||
|
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(register);
|
||||||
|
cfg.service(login);
|
||||||
|
cfg.service(signout);
|
||||||
|
}
|
||||||
|
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.auth.register")]
|
||||||
|
async fn register(payload: web::Json<Register>, ctx: AppCtx) -> ServiceResult<impl Responder> {
|
||||||
|
ctx.register(&payload).await?;
|
||||||
|
Ok(HttpResponse::Ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.auth.login")]
|
||||||
|
async fn login(
|
||||||
|
id: Identity,
|
||||||
|
payload: web::Json<Login>,
|
||||||
|
query: web::Query<RedirectQuery>,
|
||||||
|
ctx: AppCtx,
|
||||||
|
) -> ServiceResult<impl Responder> {
|
||||||
|
let payload = payload.into_inner();
|
||||||
|
let username = ctx.login(&payload).await?;
|
||||||
|
id.remember(username);
|
||||||
|
let query = query.into_inner();
|
||||||
|
if let Some(redirect_to) = query.redirect_to {
|
||||||
|
Ok(HttpResponse::Found()
|
||||||
|
.insert_header((header::LOCATION, redirect_to))
|
||||||
|
.finish())
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::Ok().into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web_codegen_const_routes::get(
|
||||||
|
path = "crate::V1_API_ROUTES.auth.logout",
|
||||||
|
wrap = "super::get_auth_middleware()"
|
||||||
|
)]
|
||||||
|
async fn signout(id: Identity) -> impl Responder {
|
||||||
|
use actix_auth_middleware::GetLoginRoute;
|
||||||
|
|
||||||
|
if id.identity().is_some() {
|
||||||
|
id.forget();
|
||||||
|
}
|
||||||
|
HttpResponse::Found()
|
||||||
|
.append_header((header::LOCATION, crate::V1_API_ROUTES.get_login_route(None)))
|
||||||
|
.finish()
|
||||||
|
}
|
45
src/api/v1/mod.rs
Normal file
45
src/api/v1/mod.rs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
use actix_auth_middleware::Authentication;
|
||||||
|
use actix_web::web::ServiceConfig;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
pub mod account;
|
||||||
|
pub mod auth;
|
||||||
|
pub mod routes;
|
||||||
|
|
||||||
|
pub use routes::ROUTES;
|
||||||
|
|
||||||
|
pub fn services(cfg: &mut ServiceConfig) {
|
||||||
|
auth::services(cfg);
|
||||||
|
account::services(cfg);
|
||||||
|
crate::meta::services(cfg);
|
||||||
|
crate::deploy::services(cfg);
|
||||||
|
crate::serve::services(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct RedirectQuery {
|
||||||
|
pub redirect_to: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_auth_middleware() -> Authentication<routes::Routes> {
|
||||||
|
Authentication::with_identity(ROUTES)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
123
src/api/v1/routes.rs
Normal file
123
src/api/v1/routes.rs
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
//! V1 API Routes
|
||||||
|
use actix_auth_middleware::GetLoginRoute;
|
||||||
|
|
||||||
|
use crate::deploy::routes::Deploy;
|
||||||
|
use crate::meta::routes::Meta;
|
||||||
|
use crate::serve::routes::Serve;
|
||||||
|
|
||||||
|
/// constant [Routes](Routes) instance
|
||||||
|
pub const ROUTES: Routes = Routes::new();
|
||||||
|
|
||||||
|
/// Authentication routes
|
||||||
|
pub struct Auth {
|
||||||
|
/// logout route
|
||||||
|
pub logout: &'static str,
|
||||||
|
/// login route
|
||||||
|
pub login: &'static str,
|
||||||
|
/// registration route
|
||||||
|
pub register: &'static str,
|
||||||
|
}
|
||||||
|
impl Auth {
|
||||||
|
/// create new instance of Authentication route
|
||||||
|
pub const fn new() -> Auth {
|
||||||
|
let login = "/api/v1/signin";
|
||||||
|
let logout = "/api/v1/logout";
|
||||||
|
let register = "/api/v1/signup";
|
||||||
|
Auth {
|
||||||
|
logout,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Account management routes
|
||||||
|
pub struct Account {
|
||||||
|
/// delete account route
|
||||||
|
pub delete: &'static str,
|
||||||
|
/// route to check if an email exists
|
||||||
|
pub email_exists: &'static str,
|
||||||
|
/// route to update a user's email
|
||||||
|
pub update_email: &'static str,
|
||||||
|
/// route to update password
|
||||||
|
pub update_password: &'static str,
|
||||||
|
/// route to check if a username is already registered
|
||||||
|
pub username_exists: &'static str,
|
||||||
|
/// route to change username
|
||||||
|
pub update_username: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Account {
|
||||||
|
/// create a new instance of [Account][Account] routes
|
||||||
|
pub const fn new() -> Account {
|
||||||
|
let delete = "/api/v1/account/delete";
|
||||||
|
let email_exists = "/api/v1/account/email/exists";
|
||||||
|
let username_exists = "/api/v1/account/username/exists";
|
||||||
|
let update_username = "/api/v1/account/username/update";
|
||||||
|
let update_email = "/api/v1/account/email/update";
|
||||||
|
let update_password = "/api/v1/account/password/update";
|
||||||
|
Account {
|
||||||
|
delete,
|
||||||
|
email_exists,
|
||||||
|
update_email,
|
||||||
|
update_password,
|
||||||
|
username_exists,
|
||||||
|
update_username,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Top-level routes data structure for V1 AP1
|
||||||
|
pub struct Routes {
|
||||||
|
/// Authentication routes
|
||||||
|
pub auth: Auth,
|
||||||
|
/// Account routes
|
||||||
|
pub account: Account,
|
||||||
|
/// Meta routes
|
||||||
|
pub meta: Meta,
|
||||||
|
pub deploy: Deploy,
|
||||||
|
pub serve: Serve,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Routes {
|
||||||
|
/// create new instance of Routes
|
||||||
|
const fn new() -> Routes {
|
||||||
|
Routes {
|
||||||
|
auth: Auth::new(),
|
||||||
|
account: Account::new(),
|
||||||
|
meta: Meta::new(),
|
||||||
|
deploy: Deploy::new(),
|
||||||
|
serve: Serve::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetLoginRoute for Routes {
|
||||||
|
fn get_login_route(&self, src: Option<&str>) -> String {
|
||||||
|
if let Some(redirect_to) = src {
|
||||||
|
format!(
|
||||||
|
"{}?redirect_to={}",
|
||||||
|
self.auth.login,
|
||||||
|
urlencoding::encode(redirect_to)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
self.auth.register.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
174
src/api/v1/tests/auth.rs
Normal file
174
src/api/v1/tests/auth.rs
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use actix_auth_middleware::GetLoginRoute;
|
||||||
|
use actix_web::http::{header, StatusCode};
|
||||||
|
use actix_web::test;
|
||||||
|
|
||||||
|
use crate::api::v1::ROUTES;
|
||||||
|
use crate::ctx::api::v1::auth::{Login, Register};
|
||||||
|
use crate::ctx::ArcCtx;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn postgrest_auth_works() {
|
||||||
|
let (_, ctx) = crate::tests::get_ctx().await;
|
||||||
|
auth_works(ctx.clone()).await;
|
||||||
|
serverside_password_validation_works(ctx).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth_works(ctx: ArcCtx) {
|
||||||
|
const NAME: &str = "testuserfoo";
|
||||||
|
const PASSWORD: &str = "longpassword";
|
||||||
|
const EMAIL: &str = "testuser1foo@a.com";
|
||||||
|
|
||||||
|
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||||
|
let app = get_app!(ctx).await;
|
||||||
|
|
||||||
|
// 1. Register and signin
|
||||||
|
let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||||
|
let cookies = get_cookie!(signin_resp);
|
||||||
|
|
||||||
|
// Sign in with email
|
||||||
|
ctx.signin_test(EMAIL, PASSWORD).await;
|
||||||
|
|
||||||
|
// 2. check if duplicate username is allowed
|
||||||
|
let mut msg = Register {
|
||||||
|
username: NAME.into(),
|
||||||
|
password: PASSWORD.into(),
|
||||||
|
confirm_password: PASSWORD.into(),
|
||||||
|
email: EMAIL.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
msg.username = format!("asdfasd{}", msg.username);
|
||||||
|
ctx.bad_post_req_test(
|
||||||
|
NAME,
|
||||||
|
PASSWORD,
|
||||||
|
ROUTES.auth.register,
|
||||||
|
&msg,
|
||||||
|
ServiceError::EmailTaken,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
msg.email = format!("asdfasd{}", msg.email);
|
||||||
|
msg.username = NAME.into();
|
||||||
|
ctx.bad_post_req_test(
|
||||||
|
NAME,
|
||||||
|
PASSWORD,
|
||||||
|
ROUTES.auth.register,
|
||||||
|
&msg,
|
||||||
|
ServiceError::UsernameTaken,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// 3. sigining in with non-existent user
|
||||||
|
let mut creds = Login {
|
||||||
|
login: "nonexistantuser".into(),
|
||||||
|
password: msg.password.clone(),
|
||||||
|
};
|
||||||
|
ctx.bad_post_req_test(
|
||||||
|
NAME,
|
||||||
|
PASSWORD,
|
||||||
|
ROUTES.auth.login,
|
||||||
|
&creds,
|
||||||
|
ServiceError::AccountNotFound,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
creds.login = "nonexistantuser@example.com".into();
|
||||||
|
ctx.bad_post_req_test(
|
||||||
|
NAME,
|
||||||
|
PASSWORD,
|
||||||
|
ROUTES.auth.login,
|
||||||
|
&creds,
|
||||||
|
ServiceError::AccountNotFound,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// 4. trying to signin with wrong password
|
||||||
|
creds.login = NAME.into();
|
||||||
|
creds.password = NAME.into();
|
||||||
|
|
||||||
|
ctx.bad_post_req_test(
|
||||||
|
NAME,
|
||||||
|
PASSWORD,
|
||||||
|
ROUTES.auth.login,
|
||||||
|
&creds,
|
||||||
|
ServiceError::WrongPassword,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// 5. signout
|
||||||
|
let signout_resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
test::TestRequest::get()
|
||||||
|
.uri(ROUTES.auth.logout)
|
||||||
|
.cookie(cookies)
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(signout_resp.status(), StatusCode::FOUND);
|
||||||
|
let headers = signout_resp.headers();
|
||||||
|
assert_eq!(
|
||||||
|
headers.get(header::LOCATION).unwrap(),
|
||||||
|
&crate::V1_API_ROUTES.get_login_route(None)
|
||||||
|
);
|
||||||
|
|
||||||
|
let creds = Login {
|
||||||
|
login: NAME.into(),
|
||||||
|
password: PASSWORD.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
//6. sigin with redirect URL set
|
||||||
|
let redirect_to = ROUTES.auth.logout;
|
||||||
|
let resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&creds, &ROUTES.get_login_route(Some(redirect_to))).to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||||
|
let headers = resp.headers();
|
||||||
|
assert_eq!(headers.get(header::LOCATION).unwrap(), &redirect_to);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serverside_password_validation_works(ctx: ArcCtx) {
|
||||||
|
const NAME: &str = "testuser542";
|
||||||
|
const PASSWORD: &str = "longpassword2";
|
||||||
|
const EMAIL: &str = "testuser542@example.com";
|
||||||
|
|
||||||
|
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||||
|
|
||||||
|
let app = get_app!(ctx).await;
|
||||||
|
|
||||||
|
// checking to see if server-side password validation (password == password_config)
|
||||||
|
// works
|
||||||
|
let register_msg = Register {
|
||||||
|
username: NAME.into(),
|
||||||
|
password: PASSWORD.into(),
|
||||||
|
confirm_password: NAME.into(),
|
||||||
|
email: EMAIL.into(),
|
||||||
|
};
|
||||||
|
let resp = test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(®ister_msg, ROUTES.auth.register).to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||||
|
let txt: ErrorToResponse = test::read_body_json(resp).await;
|
||||||
|
assert_eq!(txt.error, format!("{}", ServiceError::PasswordsDontMatch));
|
||||||
|
}
|
18
src/api/v1/tests/mod.rs
Normal file
18
src/api/v1/tests/mod.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
mod auth;
|
||||||
|
mod protected;
|
70
src/api/v1/tests/protected.rs
Normal file
70
src/api/v1/tests/protected.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use actix_web::http::StatusCode;
|
||||||
|
use actix_web::test;
|
||||||
|
|
||||||
|
use crate::ctx::ArcCtx;
|
||||||
|
//use crate::pages::PAGES;
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
use crate::tests::*;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn postgrest_protected_routes_work() {
|
||||||
|
let (_, ctx) = get_ctx().await;
|
||||||
|
protected_routes_work(ctx.clone()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn protected_routes_work(ctx: ArcCtx) {
|
||||||
|
const NAME: &str = "testuser619";
|
||||||
|
const PASSWORD: &str = "longpassword2";
|
||||||
|
const EMAIL: &str = "testuser119@a.com2";
|
||||||
|
|
||||||
|
let _post_protected_urls = [
|
||||||
|
"/api/v1/account/secret/",
|
||||||
|
"/api/v1/account/email/",
|
||||||
|
"/api/v1/account/delete",
|
||||||
|
];
|
||||||
|
|
||||||
|
let get_protected_urls = [
|
||||||
|
V1_API_ROUTES.auth.logout,
|
||||||
|
// PAGES.auth.logout,
|
||||||
|
// PAGES.home,
|
||||||
|
];
|
||||||
|
|
||||||
|
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||||
|
|
||||||
|
let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||||
|
let cookies = get_cookie!(signin_resp);
|
||||||
|
let app = get_app!(ctx).await;
|
||||||
|
|
||||||
|
for url in get_protected_urls.iter() {
|
||||||
|
let resp = get_request!(&app, url);
|
||||||
|
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||||
|
|
||||||
|
let authenticated_resp = get_request!(&app, url, cookies.clone());
|
||||||
|
|
||||||
|
println!("{url}");
|
||||||
|
if url == &V1_API_ROUTES.auth.logout {
|
||||||
|
// || url == &PAGES.auth.logout {
|
||||||
|
assert_eq!(authenticated_resp.status(), StatusCode::FOUND);
|
||||||
|
} else {
|
||||||
|
assert_eq!(authenticated_resp.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
163
src/tests.rs
163
src/tests.rs
|
@ -17,13 +17,23 @@
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use actix_web::{
|
||||||
|
body::{BoxBody, EitherBody},
|
||||||
|
dev::ServiceResponse,
|
||||||
|
error::ResponseError,
|
||||||
|
http::StatusCode,
|
||||||
|
};
|
||||||
use mktemp::Temp;
|
use mktemp::Temp;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::ctx::api::v1::auth::{Login, Register};
|
||||||
use crate::ctx::Ctx;
|
use crate::ctx::Ctx;
|
||||||
|
use crate::errors::*;
|
||||||
use crate::page::Page;
|
use crate::page::Page;
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
pub async fn get_data() -> (Temp, Arc<Ctx>) {
|
pub async fn get_ctx() -> (Temp, Arc<Ctx>) {
|
||||||
// mktemp::Temp is returned because the temp directory created
|
// mktemp::Temp is returned because the temp directory created
|
||||||
// is removed once the variable goes out of scope
|
// is removed once the variable goes out of scope
|
||||||
let mut settings = Settings::new().unwrap();
|
let mut settings = Settings::new().unwrap();
|
||||||
|
@ -46,6 +56,7 @@ pub async fn get_data() -> (Temp, Arc<Ctx>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
settings.pages = pages;
|
settings.pages = pages;
|
||||||
|
settings.init();
|
||||||
println!("[log] Initialzing settings again with test config");
|
println!("[log] Initialzing settings again with test config");
|
||||||
settings.init();
|
settings.init();
|
||||||
|
|
||||||
|
@ -58,18 +69,20 @@ pub struct FORM;
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! post_request {
|
macro_rules! post_request {
|
||||||
($uri:expr) => {
|
($uri:expr) => {
|
||||||
test::TestRequest::post().uri($uri)
|
actix_web::test::TestRequest::post().uri($uri)
|
||||||
};
|
};
|
||||||
|
|
||||||
($serializable:expr, $uri:expr) => {
|
($serializable:expr, $uri:expr) => {
|
||||||
test::TestRequest::post()
|
actix_web::test::TestRequest::post()
|
||||||
.uri($uri)
|
.uri($uri)
|
||||||
.insert_header((actix_web::http::header::CONTENT_TYPE, "application/json"))
|
.insert_header((actix_web::http::header::CONTENT_TYPE, "application/json"))
|
||||||
.set_payload(serde_json::to_string($serializable).unwrap())
|
.set_payload(serde_json::to_string($serializable).unwrap())
|
||||||
};
|
};
|
||||||
|
|
||||||
($serializable:expr, $uri:expr, FORM) => {
|
($serializable:expr, $uri:expr, FORM) => {
|
||||||
test::TestRequest::post().uri($uri).set_form($serializable)
|
actix_web::test::TestRequest::post()
|
||||||
|
.uri($uri)
|
||||||
|
.set_form($serializable)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,20 +124,17 @@ macro_rules! delete_request {
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! get_app {
|
macro_rules! get_app {
|
||||||
("APP") => {
|
|
||||||
actix_web::App::new()
|
|
||||||
.app_data($crate::get_json_err())
|
|
||||||
.wrap(actix_web::middleware::NormalizePath::new(
|
|
||||||
actix_web::middleware::TrailingSlash::Trim,
|
|
||||||
))
|
|
||||||
.configure($crate::routes::services)
|
|
||||||
};
|
|
||||||
|
|
||||||
// ($settings:ident) => {
|
|
||||||
// test::init_service(get_app!("APP", $settings))
|
|
||||||
// };
|
|
||||||
($ctx:expr) => {
|
($ctx:expr) => {
|
||||||
test::init_service(get_app!("APP").app_data($crate::WebData::new($ctx.clone())))
|
actix_web::test::init_service(
|
||||||
|
actix_web::App::new()
|
||||||
|
.app_data($crate::get_json_err())
|
||||||
|
.wrap($crate::get_identity_service(&$ctx.settings))
|
||||||
|
.wrap(actix_web::middleware::NormalizePath::new(
|
||||||
|
actix_web::middleware::TrailingSlash::Trim,
|
||||||
|
))
|
||||||
|
.configure($crate::routes::services)
|
||||||
|
.app_data($crate::WebData::new($ctx.clone())),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,3 +159,122 @@ macro_rules! check_status {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! get_cookie {
|
||||||
|
($resp:expr) => {
|
||||||
|
$resp.response().cookies().next().unwrap().to_owned()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ctx {
|
||||||
|
/// register and signin utility
|
||||||
|
pub async fn register_and_signin(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
email: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> (Login, ServiceResponse<EitherBody<BoxBody>>) {
|
||||||
|
self.register_test(name, email, password).await;
|
||||||
|
self.signin_test(name, password).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_arc(&self) -> Arc<Self> {
|
||||||
|
Arc::new(self.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// register utility
|
||||||
|
pub async fn register_test(&self, name: &str, email: &str, password: &str) {
|
||||||
|
let app = get_app!(self.to_arc()).await;
|
||||||
|
|
||||||
|
// 1. Register
|
||||||
|
let msg = Register {
|
||||||
|
username: name.into(),
|
||||||
|
password: password.into(),
|
||||||
|
confirm_password: password.into(),
|
||||||
|
email: email.into(),
|
||||||
|
};
|
||||||
|
println!("{:?}", msg);
|
||||||
|
let resp = actix_web::test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&msg, crate::V1_API_ROUTES.auth.register).to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
// let resp_err: ErrorToResponse = actix_web::test::read_body_json(resp).await;
|
||||||
|
// panic!("{}", resp_err.error);
|
||||||
|
assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// signin util
|
||||||
|
pub async fn signin_test(
|
||||||
|
&self,
|
||||||
|
|
||||||
|
name: &str,
|
||||||
|
password: &str,
|
||||||
|
) -> (Login, ServiceResponse<EitherBody<BoxBody>>) {
|
||||||
|
let app = get_app!(self.to_arc()).await;
|
||||||
|
|
||||||
|
// 2. signin
|
||||||
|
let creds = Login {
|
||||||
|
login: name.into(),
|
||||||
|
password: password.into(),
|
||||||
|
};
|
||||||
|
let signin_resp = actix_web::test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&creds, V1_API_ROUTES.auth.login).to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(signin_resp.status(), StatusCode::OK);
|
||||||
|
(creds, signin_resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// pub duplicate test
|
||||||
|
pub async fn bad_post_req_test<T: Serialize>(
|
||||||
|
&self,
|
||||||
|
|
||||||
|
name: &str,
|
||||||
|
password: &str,
|
||||||
|
url: &str,
|
||||||
|
payload: &T,
|
||||||
|
err: ServiceError,
|
||||||
|
) {
|
||||||
|
let (_, signin_resp) = self.signin_test(name, password).await;
|
||||||
|
let cookies = get_cookie!(signin_resp);
|
||||||
|
let app = get_app!(self.to_arc()).await;
|
||||||
|
|
||||||
|
let resp = actix_web::test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(&payload, url)
|
||||||
|
.cookie(cookies.clone())
|
||||||
|
.to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), err.status_code());
|
||||||
|
let resp_err: ErrorToResponse = actix_web::test::read_body_json(resp).await;
|
||||||
|
//println!("{}", txt.error);
|
||||||
|
assert_eq!(resp_err.error, format!("{}", err));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// bad post req test without payload
|
||||||
|
pub async fn bad_post_req_test_witout_payload(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
password: &str,
|
||||||
|
url: &str,
|
||||||
|
err: ServiceError,
|
||||||
|
) {
|
||||||
|
let (_, signin_resp) = self.signin_test(name, password).await;
|
||||||
|
let app = get_app!(self.to_arc()).await;
|
||||||
|
let cookies = get_cookie!(signin_resp);
|
||||||
|
|
||||||
|
let resp = actix_web::test::call_service(
|
||||||
|
&app,
|
||||||
|
post_request!(url).cookie(cookies.clone()).to_request(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(resp.status(), err.status_code());
|
||||||
|
let resp_err: ErrorToResponse = actix_web::test::read_body_json(resp).await;
|
||||||
|
//println!("{}", resp_err.error);
|
||||||
|
assert_eq!(resp_err.error, format!("{}", err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue