diff --git a/Cargo.lock b/Cargo.lock index 26a19a08..7c3cd317 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1484,8 +1484,8 @@ dependencies = [ [[package]] name = "m_captcha" -version = "0.1.0" -source = "git+https://github.com/mCaptcha/mCaptcha?tag=0.1.0#8c4f885a854041737d822d7a9b36db3fa524e15d" +version = "0.1.1" +source = "git+https://github.com/mCaptcha/mCaptcha#f401124d9f1184051fd37ac65349b3273ff1ca9f" dependencies = [ "actix", "derive_builder", @@ -2059,21 +2059,20 @@ dependencies = [ [[package]] name = "regex" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +checksum = "54fd1046a3107eb58f42de31d656fee6853e5d276c455fd943742dce89fc3dd3" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] name = "regex-syntax" -version = "0.6.22" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" +checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" [[package]] name = "resolv-conf" @@ -2614,15 +2613,6 @@ dependencies = [ "syn", ] -[[package]] -name = "thread_local" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" -dependencies = [ - "once_cell", -] - [[package]] name = "threadpool" version = "1.8.1" diff --git a/Cargo.toml b/Cargo.toml index b13901c6..7c349ddb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ log = "0.4" lazy_static = "1.4" -m_captcha = { version = "0.1.0", git = "https://github.com/mCaptcha/mCaptcha", tag = "0.1.0" } +m_captcha = { version = "0.1.1", git = "https://github.com/mCaptcha/mCaptcha" } rand = "0.8" diff --git a/src/api/v1/auth.rs b/src/api/v1/auth.rs index 60f69d7a..12d59b9e 100644 --- a/src/api/v1/auth.rs +++ b/src/api/v1/auth.rs @@ -153,116 +153,3 @@ pub async fn delete_account( Err(_) => return Err(ServiceError::InternalServerError)?, } } - -#[cfg(test)] -mod tests { - use actix_web::http::{header, StatusCode}; - use actix_web::test; - - use super::*; - use crate::api::v1::services as v1_services; - use crate::data::Data; - use crate::*; - - use crate::tests::*; - - #[actix_rt::test] - async fn auth_works() { - let data = Data::new().await; - const NAME: &str = "testuser"; - const PASSWORD: &str = "longpassword"; - const EMAIL: &str = "testuser1@a.com"; - const SIGNIN: &str = "/api/v1/signin"; - const SIGNUP: &str = "/api/v1/signup"; - - let mut app = get_app!(data).await; - - delete_user(NAME, &data).await; - - // 1. Register and signin - let (_, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await; - let cookies = get_cookie!(signin_resp); - - // 2. check if duplicate username is allowed - let msg = Register { - username: NAME.into(), - password: PASSWORD.into(), - email: EMAIL.into(), - }; - bad_post_req_test( - NAME, - PASSWORD, - SIGNUP, - &msg, - ServiceError::UsernameTaken, - StatusCode::BAD_REQUEST, - ) - .await; - - // 3. sigining in with non-existent user - let mut login = Login { - username: "nonexistantuser".into(), - password: msg.password.clone(), - }; - bad_post_req_test( - NAME, - PASSWORD, - SIGNIN, - &login, - ServiceError::UsernameNotFound, - StatusCode::UNAUTHORIZED, - ) - .await; - - // 4. trying to signin with wrong password - login.username = NAME.into(); - login.password = NAME.into(); - - bad_post_req_test( - NAME, - PASSWORD, - SIGNIN, - &login, - ServiceError::WrongPassword, - StatusCode::UNAUTHORIZED, - ) - .await; - - // 5. signout - let signout_resp = test::call_service( - &mut app, - test::TestRequest::post() - .uri("/api/v1/signout") - .cookie(cookies) - .to_request(), - ) - .await; - assert_eq!(signout_resp.status(), StatusCode::OK); - } - - #[actix_rt::test] - async fn del_userworks() { - const NAME: &str = "testuser2"; - const PASSWORD: &str = "longpassword2"; - const EMAIL: &str = "testuser1@a.com2"; - - { - let data = Data::new().await; - delete_user(NAME, &data).await; - } - - let (data, creds, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await; - let cookies = get_cookie!(signin_resp); - let mut app = get_app!(data).await; - - let delete_user_resp = test::call_service( - &mut app, - post_request!(&creds, "/api/v1/account/delete") - .cookie(cookies) - .to_request(), - ) - .await; - - assert_eq!(delete_user_resp.status(), StatusCode::OK); - } -} diff --git a/src/api/v1/mcaptcha.rs b/src/api/v1/mcaptcha.rs index 2059cd1d..1b1564e7 100644 --- a/src/api/v1/mcaptcha.rs +++ b/src/api/v1/mcaptcha.rs @@ -17,6 +17,7 @@ use actix_identity::Identity; use actix_web::{post, web, HttpResponse, Responder}; +use m_captcha::{defense::Level, DefenseBuilder}; use serde::{Deserialize, Serialize}; use url::Url; @@ -40,8 +41,8 @@ pub async fn add_domain( if let Some(host) = url.host_str() { let user = id.identity().unwrap(); let res = sqlx::query!( - "insert into mcaptcha_domains (name, ID) values - ($1, (select ID from mcaptcha_users where name = ($2) ));", + "INSERT INTO mcaptcha_domains (name, ID) VALUES + ($1, (SELECT ID FROM mcaptcha_users WHERE name = ($2) ));", host, user ) @@ -155,139 +156,225 @@ fn get_random(len: usize) -> String { .collect::() } -#[cfg(test)] -mod tests { - use actix_web::http::{header, StatusCode}; - use actix_web::test; +#[derive(Serialize, Deserialize)] +pub struct AddLevels { + pub levels: Vec, + // name is config_name + pub name: String, +} - use super::*; - use crate::api::v1::services as v1_services; - use crate::tests::*; - use crate::*; +#[post("/api/v1/mcaptcha/domain/token/levels/add")] +pub async fn add_levels( + payload: web::Json, + data: web::Data, + id: Identity, +) -> ServiceResult { + is_authenticated(&id)?; + let mut defense = DefenseBuilder::default(); - #[actix_rt::test] - async fn add_domains_work() { - const NAME: &str = "testuserdomainn"; - const PASSWORD: &str = "longpassworddomain"; - const EMAIL: &str = "testuserdomain@a.com"; - const DOMAIN: &str = "http://example.com"; - const ADD_URL: &str = "/api/v1/mcaptcha/domain/add"; - - { - let data = Data::new().await; - delete_user(NAME, &data).await; - } - - register_and_signin(NAME, EMAIL, PASSWORD).await; - - // 1. add domain - let (data, _, signin_resp) = add_domain_util(NAME, PASSWORD, DOMAIN).await; - let cookies = get_cookie!(signin_resp); - let mut app = get_app!(data).await; - - let mut domain = Domain { - name: DOMAIN.into(), - }; - - // 2. duplicate domain - bad_post_req_test( - NAME, - PASSWORD, - ADD_URL, - &domain, - ServiceError::HostnameTaken, - StatusCode::BAD_REQUEST, - ) - .await; - - // 3. delete domain - let del_domain_resp = test::call_service( - &mut app, - post_request!(&domain, "/api/v1/mcaptcha/domain/delete") - .cookie(cookies.clone()) - .to_request(), - ) - .await; - assert_eq!(del_domain_resp.status(), StatusCode::OK); - - // 4. not a URL test for adding domain - domain.name = "testing".into(); - bad_post_req_test( - NAME, - PASSWORD, - ADD_URL, - &domain, - ServiceError::NotAUrl, - StatusCode::BAD_REQUEST, - ) - .await; + for level in payload.levels.iter() { + defense.add_level(level.clone())?; } - #[actix_rt::test] - async fn add_mcaptcha_works() { - const NAME: &str = "testusermcaptcha"; - const PASSWORD: &str = "longpassworddomain"; - const EMAIL: &str = "testusermcaptcha@a.com"; - const DOMAIN: &str = "http://mcaptcha.example.com"; - const TOKEN_NAME: &str = "add_mcaptcha_works_token"; - const ADD_URL: &str = "/api/v1/mcaptcha/domain/token/add"; - const DEL_URL: &str = "/api/v1/mcaptcha/domain/token/delete"; + defense.build()?; - { - let data = Data::new().await; - delete_user(NAME, &data).await; - } - - register_and_signin(NAME, EMAIL, PASSWORD).await; - let (data, _, signin_resp) = add_domain_util(NAME, PASSWORD, DOMAIN).await; - let cookies = get_cookie!(signin_resp); - let mut app = get_app!(data).await; - - // 1. add mcaptcha token - let mut domain = CreateToken { - domain: DOMAIN.into(), - name: TOKEN_NAME.into(), - }; - let add_token_resp = test::call_service( - &mut app, - post_request!(&domain, ADD_URL) - .cookie(cookies.clone()) - .to_request(), + for level in payload.levels.iter() { + let difficulty_factor = level.difficulty_factor as i32; + let visitor_threshold = level.visitor_threshold as i32; + sqlx::query!( + "INSERT INTO mcaptcha_levels ( + difficulty_factor, + visitor_threshold, + config_id) VALUES ($1, $2, (SELECT config_id FROM mcaptcha_config WHERE name = ($3) ));", + difficulty_factor, + visitor_threshold, + &payload.name, ) - .await; - assert_eq!(add_token_resp.status(), StatusCode::OK); + .execute(&data.db) + .await?; + } - // 2. add duplicate mcaptha - bad_post_req_test( - NAME, - PASSWORD, - ADD_URL, - &domain, - ServiceError::TokenNameTaken, - StatusCode::BAD_REQUEST, - ) - .await; + Ok(HttpResponse::Ok()) +} - // 4. not a URL test for adding domain - domain.domain = "testing".into(); - bad_post_req_test( - NAME, - PASSWORD, - ADD_URL, - &domain, - ServiceError::NotAUrl, - StatusCode::BAD_REQUEST, - ) - .await; +#[post("/api/v1/mcaptcha/domain/token/levels/update")] +pub async fn update_levels( + payload: web::Json, + data: web::Data, + id: Identity, +) -> ServiceResult { + is_authenticated(&id)?; + let mut defense = DefenseBuilder::default(); - // 4. delete token - let del_token = test::call_service( - &mut app, - post_request!(&domain, DEL_URL) - .cookie(cookies.clone()) - .to_request(), + for level in payload.levels.iter() { + defense.add_level(level.clone())?; + } + + // I feel this is necessary as both difficulty factor _and_ visitor threshold of a + // level could change so doing this would not require us to send level_id to client + // still, needs to be benchmarked + defense.build()?; + + sqlx::query!( + "DELETE FROM mcaptcha_levels + WHERE config_id = ( + SELECT config_id FROM mcaptcha_config where name = ($1) + )", + &payload.name, + ) + .execute(&data.db) + .await?; + + for level in payload.levels.iter() { + let difficulty_factor = level.difficulty_factor as i32; + let visitor_threshold = level.visitor_threshold as i32; + sqlx::query!( + "INSERT INTO mcaptcha_levels ( + difficulty_factor, + visitor_threshold, + config_id) VALUES ($1, $2, (SELECT config_id FROM mcaptcha_config WHERE name = ($3) ));", + difficulty_factor, + visitor_threshold, + &payload.name, ) - .await; - assert_eq!(del_token.status(), StatusCode::OK); + .execute(&data.db) + .await?; + } + + Ok(HttpResponse::Ok()) +} + +#[post("/api/v1/mcaptcha/domain/token/levels/delete")] +pub async fn delete_levels( + payload: web::Json, + data: web::Data, + id: Identity, +) -> ServiceResult { + is_authenticated(&id)?; + + for level in payload.levels.iter() { + let difficulty_factor = level.difficulty_factor as i32; + sqlx::query!( + "DELETE FROM mcaptcha_levels WHERE + config_id = ( + SELECT config_id FROM mcaptcha_config WHERE name = ($1) + ) AND difficulty_factor = ($2);", + &payload.name, + difficulty_factor, + ) + .execute(&data.db) + .await?; + } + + Ok(HttpResponse::Ok()) +} + +#[derive(Deserialize, Serialize)] +pub struct GetLevels { + pub token: String, +} + +#[post("/api/v1/mcaptcha/domain/token/levels/get")] +pub async fn get_levels( + payload: web::Json, + data: web::Data, + id: Identity, +) -> ServiceResult { + is_authenticated(&id)?; + + let levels = get_levels_util(&payload.token, &data).await?; + + Ok(HttpResponse::Ok().json(levels)) +} + +#[derive(Deserialize, Serialize)] +pub struct Duration { + pub token_name: String, + pub duration: i32, +} + +#[post("/api/v1/mcaptcha/domain/token/duration/update")] +pub async fn update_duration( + payload: web::Json, + data: web::Data, + id: Identity, +) -> ServiceResult { + is_authenticated(&id)?; + + if payload.duration > 0 { + sqlx::query!( + "UPDATE mcaptcha_config set duration = $1 WHERE + name = $2;", + &payload.duration, + &payload.token_name, + ) + .execute(&data.db) + .await?; + + Ok(HttpResponse::Ok()) + } else { + // when mCaptcha/mCaptcha #2 is fixed, this wont be necessary + Err(ServiceError::CaptchaError( + m_captcha::errors::CaptchaError::DifficultyFactorZero, + )) } } + +#[derive(Deserialize, Serialize)] +pub struct GetDuration { + pub duration: i32, +} + +#[post("/api/v1/mcaptcha/domain/token/duration/get")] +pub async fn get_duration( + payload: web::Json, + data: web::Data, + id: Identity, +) -> ServiceResult { + is_authenticated(&id)?; + + let duration = sqlx::query_as!( + GetDuration, + "SELECT duration FROM mcaptcha_config WHERE + name = $1;", + &payload.token, + ) + .fetch_one(&data.db) + .await?; + Ok(HttpResponse::Ok().json(duration)) +} + +#[derive(Deserialize, Serialize)] +pub struct Levels { + levels: I32Levels, +} + +#[derive(Deserialize, Serialize)] +pub struct I32Levels { + difficulty_factor: i32, + visitor_threshold: i32, +} + +async fn get_levels_util(name: &str, data: &Data) -> ServiceResult> { + let levels = sqlx::query_as!( + I32Levels, + "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE + config_id = ( + SELECT config_id FROM mcaptcha_config WHERE name = ($1) + );", + name + ) + .fetch_all(&data.db) + .await?; + + Ok(levels) +} + +// Workflow: +// 1. Sign up +// 2. Sign in +// 3. Add domain(DNS TXT record verification? / put string at path) +// 4. Create token +// 5. Add levels +// 6. Update duration +// 7. Start syatem diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index d83f2aa4..84e9c363 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -33,4 +33,15 @@ pub fn services(cfg: &mut ServiceConfig) { cfg.service(delete_domain); cfg.service(add_mcaptcha); cfg.service(delete_mcaptcha); + + cfg.service(add_levels); + cfg.service(update_levels); + cfg.service(delete_levels); + cfg.service(get_levels); + + cfg.service(update_duration); + cfg.service(get_duration); } + +#[cfg(test)] +mod tests; diff --git a/src/api/v1/tests/auth.rs b/src/api/v1/tests/auth.rs new file mode 100644 index 00000000..3774776b --- /dev/null +++ b/src/api/v1/tests/auth.rs @@ -0,0 +1,127 @@ +/* +* Copyright (C) 2021 Aravinth Manivannan +* +* 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 . +*/ + +use actix_web::http::{header, StatusCode}; +use actix_web::test; + +use crate::api::v1::auth::*; +use crate::api::v1::services as v1_services; +use crate::data::Data; +use crate::errors::*; +use crate::*; + +use crate::tests::*; + +#[actix_rt::test] +async fn auth_works() { + let data = Data::new().await; + const NAME: &str = "testuser"; + const PASSWORD: &str = "longpassword"; + const EMAIL: &str = "testuser1@a.com"; + const SIGNIN: &str = "/api/v1/signin"; + const SIGNUP: &str = "/api/v1/signup"; + + let mut app = get_app!(data).await; + + delete_user(NAME, &data).await; + + // 1. Register and signin + let (_, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + + // 2. check if duplicate username is allowed + let msg = Register { + username: NAME.into(), + password: PASSWORD.into(), + email: EMAIL.into(), + }; + bad_post_req_test( + NAME, + PASSWORD, + SIGNUP, + &msg, + ServiceError::UsernameTaken, + StatusCode::BAD_REQUEST, + ) + .await; + + // 3. sigining in with non-existent user + let mut login = Login { + username: "nonexistantuser".into(), + password: msg.password.clone(), + }; + bad_post_req_test( + NAME, + PASSWORD, + SIGNIN, + &login, + ServiceError::UsernameNotFound, + StatusCode::UNAUTHORIZED, + ) + .await; + + // 4. trying to signin with wrong password + login.username = NAME.into(); + login.password = NAME.into(); + + bad_post_req_test( + NAME, + PASSWORD, + SIGNIN, + &login, + ServiceError::WrongPassword, + StatusCode::UNAUTHORIZED, + ) + .await; + + // 5. signout + let signout_resp = test::call_service( + &mut app, + test::TestRequest::post() + .uri("/api/v1/signout") + .cookie(cookies) + .to_request(), + ) + .await; + assert_eq!(signout_resp.status(), StatusCode::OK); +} + +#[actix_rt::test] +async fn del_userworks() { + const NAME: &str = "testuser2"; + const PASSWORD: &str = "longpassword2"; + const EMAIL: &str = "testuser1@a.com2"; + + { + let data = Data::new().await; + delete_user(NAME, &data).await; + } + + let (data, creds, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let mut app = get_app!(data).await; + + let delete_user_resp = test::call_service( + &mut app, + post_request!(&creds, "/api/v1/account/delete") + .cookie(cookies) + .to_request(), + ) + .await; + + assert_eq!(delete_user_resp.status(), StatusCode::OK); +} diff --git a/src/api/v1/tests/mcaptcha.rs b/src/api/v1/tests/mcaptcha.rs new file mode 100644 index 00000000..1e5f4d86 --- /dev/null +++ b/src/api/v1/tests/mcaptcha.rs @@ -0,0 +1,338 @@ +/* +* Copyright (C) 2021 Aravinth Manivannan +* +* 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 . +*/ + +use actix_web::http::{header, StatusCode}; +use actix_web::test; +use m_captcha::defense::Level; + +use crate::api::v1::mcaptcha::*; +use crate::api::v1::services as v1_services; +use crate::errors::*; +use crate::tests::*; +use crate::*; + +#[actix_rt::test] +async fn add_domains_work() { + const NAME: &str = "testuserdomainn"; + const PASSWORD: &str = "longpassworddomain"; + const EMAIL: &str = "testuserdomain@a.com"; + const DOMAIN: &str = "http://example.com"; + const ADD_URL: &str = "/api/v1/mcaptcha/domain/add"; + + { + let data = Data::new().await; + delete_user(NAME, &data).await; + } + + register_and_signin(NAME, EMAIL, PASSWORD).await; + + // 1. add domain + let (data, _, signin_resp) = add_domain_util(NAME, PASSWORD, DOMAIN).await; + let cookies = get_cookie!(signin_resp); + let mut app = get_app!(data).await; + + let mut domain = Domain { + name: DOMAIN.into(), + }; + + // 2. duplicate domain + bad_post_req_test( + NAME, + PASSWORD, + ADD_URL, + &domain, + ServiceError::HostnameTaken, + StatusCode::BAD_REQUEST, + ) + .await; + + // 3. delete domain + let del_domain_resp = test::call_service( + &mut app, + post_request!(&domain, "/api/v1/mcaptcha/domain/delete") + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(del_domain_resp.status(), StatusCode::OK); + + // 4. not a URL test for adding domain + domain.name = "testing".into(); + bad_post_req_test( + NAME, + PASSWORD, + ADD_URL, + &domain, + ServiceError::NotAUrl, + StatusCode::BAD_REQUEST, + ) + .await; +} + +#[actix_rt::test] +async fn add_mcaptcha_works() { + const NAME: &str = "testusermcaptcha"; + const PASSWORD: &str = "longpassworddomain"; + const EMAIL: &str = "testusermcaptcha@a.com"; + const DOMAIN: &str = "http://mcaptcha.example.com"; + const TOKEN_NAME: &str = "add_mcaptcha_works_token"; + const ADD_URL: &str = "/api/v1/mcaptcha/domain/token/add"; + const DEL_URL: &str = "/api/v1/mcaptcha/domain/token/delete"; + + { + let data = Data::new().await; + delete_user(NAME, &data).await; + } + + // 1. add mcaptcha token + register_and_signin(NAME, EMAIL, PASSWORD).await; + let (data, _, signin_resp) = add_token_util(NAME, PASSWORD, DOMAIN, TOKEN_NAME).await; + let cookies = get_cookie!(signin_resp); + let mut app = get_app!(data).await; + + let mut domain = CreateToken { + domain: DOMAIN.into(), + name: TOKEN_NAME.into(), + }; + + // 2. add duplicate mcaptha + bad_post_req_test( + NAME, + PASSWORD, + ADD_URL, + &domain, + ServiceError::TokenNameTaken, + StatusCode::BAD_REQUEST, + ) + .await; + + // 4. not a URL test for adding domain + domain.domain = "testing".into(); + bad_post_req_test( + NAME, + PASSWORD, + ADD_URL, + &domain, + ServiceError::NotAUrl, + StatusCode::BAD_REQUEST, + ) + .await; + + // 4. delete token + let del_token = test::call_service( + &mut app, + post_request!(&domain, DEL_URL) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(del_token.status(), StatusCode::OK); +} + +#[actix_rt::test] +async fn level_routes_work() { + const NAME: &str = "testuserlevelroutes"; + const PASSWORD: &str = "longpassworddomain"; + const EMAIL: &str = "testuserlevelrouts@a.com"; + const DOMAIN: &str = "http://level.example.com"; + const TOKEN_NAME: &str = "level_routes_work"; + const ADD_URL: &str = "/api/v1/mcaptcha/domain/token/levels/add"; + const UPDATE_URL: &str = "/api/v1/mcaptcha/domain/token/levels/update"; + const DEL_URL: &str = "/api/v1/mcaptcha/domain/token/levels/delete"; + const GET_URL: &str = "/api/v1/mcaptcha/domain/token/levels/get"; + + let l1 = Level { + difficulty_factor: 50, + visitor_threshold: 50, + }; + let l2 = Level { + difficulty_factor: 500, + visitor_threshold: 500, + }; + let levels = vec![l1, l2]; + let add_level = AddLevels { + levels: levels.clone(), + name: TOKEN_NAME.into(), + }; + + let get_level = GetLevels { + token: TOKEN_NAME.into(), + }; + + { + let data = Data::new().await; + delete_user(NAME, &data).await; + } + + register_and_signin(NAME, EMAIL, PASSWORD).await; + let (data, _, signin_resp) = add_token_util(NAME, PASSWORD, DOMAIN, TOKEN_NAME).await; + let cookies = get_cookie!(signin_resp); + let mut app = get_app!(data).await; + + // 1. add level + let add_token_resp = test::call_service( + &mut app, + post_request!(&add_level, ADD_URL) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(add_token_resp.status(), StatusCode::OK); + + // 2. get level + let get_level_resp = test::call_service( + &mut app, + post_request!(&get_level, GET_URL) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(get_level_resp.status(), StatusCode::OK); + let res_levels: Vec = test::read_body_json(get_level_resp).await; + assert_eq!(res_levels, levels); + + // 3. update level + + let l1 = Level { + difficulty_factor: 10, + visitor_threshold: 10, + }; + let l2 = Level { + difficulty_factor: 5000, + visitor_threshold: 5000, + }; + let levels = vec![l1, l2]; + let add_level = AddLevels { + levels: levels.clone(), + name: TOKEN_NAME.into(), + }; + let add_token_resp = test::call_service( + &mut app, + post_request!(&add_level, UPDATE_URL) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(add_token_resp.status(), StatusCode::OK); + let get_level_resp = test::call_service( + &mut app, + post_request!(&get_level, GET_URL) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(get_level_resp.status(), StatusCode::OK); + let res_levels: Vec = test::read_body_json(get_level_resp).await; + assert_eq!(res_levels, levels); + + // 4. delete level + let l1 = Level { + difficulty_factor: 10, + visitor_threshold: 10, + }; + let l2 = Level { + difficulty_factor: 5000, + visitor_threshold: 5000, + }; + let levels = vec![l1, l2]; + let add_level = AddLevels { + levels: levels.clone(), + name: TOKEN_NAME.into(), + }; + let add_token_resp = test::call_service( + &mut app, + post_request!(&add_level, DEL_URL) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(add_token_resp.status(), StatusCode::OK); + let get_level_resp = test::call_service( + &mut app, + post_request!(&get_level, GET_URL) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(get_level_resp.status(), StatusCode::OK); + let res_levels: Vec = test::read_body_json(get_level_resp).await; + assert_eq!(res_levels, Vec::new()); +} + +#[actix_rt::test] +async fn update_duration() { + const NAME: &str = "testuserduration"; + const PASSWORD: &str = "longpassworddomain"; + const EMAIL: &str = "testuserduration@a.com"; + const DOMAIN: &str = "http://duration.example.com"; + const TOKEN_NAME: &str = "duration_routes_token"; + const GET_URL: &str = "/api/v1/mcaptcha/domain/token/duration/get"; + const UPDATE_URL: &str = "/api/v1/mcaptcha/domain/token/duration/update"; + + { + let data = Data::new().await; + delete_user(NAME, &data).await; + } + + register_and_signin(NAME, EMAIL, PASSWORD).await; + let (data, _, signin_resp) = add_token_util(NAME, PASSWORD, DOMAIN, TOKEN_NAME).await; + let cookies = get_cookie!(signin_resp); + let mut app = get_app!(data).await; + + let update = Duration { + token_name: TOKEN_NAME.into(), + duration: 40, + }; + + let get = GetLevels { + token: TOKEN_NAME.into(), + }; + + // check default + + let get_level_resp = test::call_service( + &mut app, + post_request!(&get, GET_URL) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(get_level_resp.status(), StatusCode::OK); + let res_levels: GetDuration = test::read_body_json(get_level_resp).await; + assert_eq!(res_levels.duration, 30); + + // update and check changes + + let update_duration = test::call_service( + &mut app, + post_request!(&update, UPDATE_URL) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(update_duration.status(), StatusCode::OK); + let get_level_resp = test::call_service( + &mut app, + post_request!(&get, GET_URL) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(get_level_resp.status(), StatusCode::OK); + let res_levels: GetDuration = test::read_body_json(get_level_resp).await; + assert_eq!(res_levels.duration, 40); +} diff --git a/src/api/v1/tests/mod.rs b/src/api/v1/tests/mod.rs new file mode 100644 index 00000000..2ab2ce23 --- /dev/null +++ b/src/api/v1/tests/mod.rs @@ -0,0 +1,19 @@ +/* +* Copyright (C) 2021 Aravinth Manivannan +* +* 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 . +*/ + +mod auth; +mod mcaptcha; diff --git a/src/errors.rs b/src/errors.rs index c121df09..b669116c 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -15,23 +15,22 @@ * along with this program. If not, see . */ +use std::convert::From; + use actix_web::{ dev::HttpResponseBuilder, error::ResponseError, http::{header, StatusCode}, HttpResponse, }; - use argon2_creds::errors::CredsError; -use url::ParseError; - use derive_more::{Display, Error}; use log::debug; +use m_captcha::errors::CaptchaError; use serde::{Deserialize, Serialize}; +use url::ParseError; use validator::ValidationErrors; -use std::convert::From; - #[derive(Debug, Display, Clone, PartialEq, Error)] #[cfg(not(tarpaulin_include))] pub enum ServiceError { @@ -72,13 +71,15 @@ pub enum ServiceError { /// when the a username is already taken #[display(fmt = "Username not available")] UsernameTaken, - /// when the a token name is already taken #[display(fmt = "token name not available")] TokenNameTaken, /// when the a host name is already taken #[display(fmt = "host name not available")] HostnameTaken, + + #[display(fmt = "{}", _0)] + CaptchaError(CaptchaError), } #[derive(Serialize, Deserialize)] @@ -101,7 +102,7 @@ impl ResponseError for ServiceError { #[cfg(not(tarpaulin_include))] fn status_code(&self) -> StatusCode { println!("{:?}", &self); - match *self { + match self { ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, ServiceError::NotAnEmail => StatusCode::BAD_REQUEST, ServiceError::NotAUrl => StatusCode::BAD_REQUEST, @@ -116,6 +117,11 @@ impl ResponseError for ServiceError { ServiceError::UsernameTaken => StatusCode::BAD_REQUEST, ServiceError::TokenNameTaken => StatusCode::BAD_REQUEST, ServiceError::HostnameTaken => StatusCode::BAD_REQUEST, + ServiceError::CaptchaError(e) => match e { + CaptchaError::MailboxError => StatusCode::INTERNAL_SERVER_ERROR, + + _ => StatusCode::BAD_REQUEST, + }, } } } @@ -148,6 +154,12 @@ impl From for ServiceError { } } +impl From for ServiceError { + fn from(e: CaptchaError) -> ServiceError { + ServiceError::CaptchaError(e) + } +} + #[cfg(not(tarpaulin_include))] impl From for ServiceError { #[cfg(not(tarpaulin_include))] diff --git a/src/tests/mod.rs b/src/tests/mod.rs index b3ca2f96..e1a79681 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -60,7 +60,7 @@ pub async fn register_and_signin<'a>( signin(name, password).await } -/// register and signin utility +/// register utility pub async fn register<'a>(name: &'a str, email: &str, password: &str) { let data = Data::new().await; let mut app = get_app!(data).await; @@ -125,6 +125,37 @@ pub async fn add_domain_util( (data, creds, signin_resp) } +pub async fn add_token_util( + name: &str, + password: &str, + domain: &str, + token_name: &str, +) -> (data::Data, Login, ServiceResponse) { + use crate::api::v1::mcaptcha::CreateToken; + + const ADD_URL: &str = "/api/v1/mcaptcha/domain/token/add"; + + let (data, creds, signin_resp) = add_domain_util(name, password, domain).await; + let cookies = get_cookie!(signin_resp); + let mut app = get_app!(data).await; + + // 1. add mcaptcha token + let domain = CreateToken { + domain: domain.into(), + name: token_name.into(), + }; + let add_token_resp = test::call_service( + &mut app, + post_request!(&domain, ADD_URL) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(add_token_resp.status(), StatusCode::OK); + + (data, creds, signin_resp) +} + /// pub duplicate test pub async fn bad_post_req_test( name: &str,