feat: auth and account management API

This commit is contained in:
Aravinth Manivannan 2022-09-16 13:23:19 +05:30
parent 7185bac60b
commit 04d7f872d5
Signed by: realaravinth
GPG Key ID: AD9F0F08E855ED88
11 changed files with 1100 additions and 19 deletions

View File

@ -83,11 +83,11 @@ jobs:
run: make docker
- 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
- 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
env:
DUMBSERVE_PASSWORD: ${{ secrets.DUMBSERVE_PASSWORD }}

17
src/api/mod.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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!(&register_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
View 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;

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

View File

@ -17,13 +17,23 @@
use std::path::Path;
use std::sync::Arc;
use actix_web::{
body::{BoxBody, EitherBody},
dev::ServiceResponse,
error::ResponseError,
http::StatusCode,
};
use mktemp::Temp;
use serde::Serialize;
use crate::ctx::api::v1::auth::{Login, Register};
use crate::ctx::Ctx;
use crate::errors::*;
use crate::page::Page;
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
// is removed once the variable goes out of scope
let mut settings = Settings::new().unwrap();
@ -46,6 +56,7 @@ pub async fn get_data() -> (Temp, Arc<Ctx>) {
}
settings.pages = pages;
settings.init();
println!("[log] Initialzing settings again with test config");
settings.init();
@ -58,18 +69,20 @@ pub struct FORM;
#[macro_export]
macro_rules! post_request {
($uri:expr) => {
test::TestRequest::post().uri($uri)
actix_web::test::TestRequest::post().uri($uri)
};
($serializable:expr, $uri:expr) => {
test::TestRequest::post()
actix_web::test::TestRequest::post()
.uri($uri)
.insert_header((actix_web::http::header::CONTENT_TYPE, "application/json"))
.set_payload(serde_json::to_string($serializable).unwrap())
};
($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_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) => {
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));
}
}