diff --git a/migrations/20210310122339_mcaptcha_domains.sql b/migrations/20210310122339_mcaptcha_domains.sql index df482b4f..615c6e50 100644 --- a/migrations/20210310122339_mcaptcha_domains.sql +++ b/migrations/20210310122339_mcaptcha_domains.sql @@ -1,4 +1,4 @@ CREATE TABLE IF NOT EXISTS mcaptcha_domains ( name VARCHAR(100) PRIMARY KEY NOT NULL UNIQUE, - ID INTEGER references mcaptcha_users(ID) NOT NULL + ID INTEGER references mcaptcha_users(ID) ON DELETE CASCADE NOT NULL ); diff --git a/migrations/20210310122617_mcaptcha_config.sql b/migrations/20210310122617_mcaptcha_config.sql index 7f260ba7..3f170e6d 100644 --- a/migrations/20210310122617_mcaptcha_config.sql +++ b/migrations/20210310122617_mcaptcha_config.sql @@ -1,6 +1,7 @@ CREATE TABLE IF NOT EXISTS mcaptcha_config ( config_id SERIAL PRIMARY KEY NOT NULL, - ID INTEGER references mcaptcha_users(ID), + domain_name VARCHAR(100) NOT NULL references mcaptcha_domains(name) ON DELETE CASCADE, key VARCHAR(100) NOT NULL UNIQUE, + name VARCHAR(100) NOT NULL UNIQUE, duration INTEGER NOT NULL DEFAULT 30 ); diff --git a/migrations/20210310122902_mcaptcha_levels.sql b/migrations/20210310122902_mcaptcha_levels.sql index d39dd2f8..e04963ac 100644 --- a/migrations/20210310122902_mcaptcha_levels.sql +++ b/migrations/20210310122902_mcaptcha_levels.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS mcaptcha_levels ( - config_id INTEGER references mcaptcha_config(config_id), + config_id INTEGER references mcaptcha_config(config_id) ON DELETE CASCADE, difficulty_factor INTEGER NOT NULL, visitor_threshold INTEGER NOT NULL, level_id SERIAL PRIMARY KEY NOT NULL diff --git a/src/api/v1/auth.rs b/src/api/v1/auth.rs index 8c580554..60f69d7a 100644 --- a/src/api/v1/auth.rs +++ b/src/api/v1/auth.rs @@ -172,13 +172,15 @@ mod tests { 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 (data, _, signin_resp) = signin_util(NAME, EMAIL, PASSWORD).await; + let (_, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await; let cookies = get_cookie!(signin_resp); // 2. check if duplicate username is allowed @@ -187,49 +189,55 @@ mod tests { password: PASSWORD.into(), email: EMAIL.into(), }; - let duplicate_user_resp = - test::call_service(&mut app, post_request!(&msg, "/api/v1/signup").to_request()).await; - assert_eq!(duplicate_user_resp.status(), StatusCode::BAD_REQUEST); + bad_post_req_test( + NAME, + PASSWORD, + SIGNUP, + &msg, + ServiceError::UsernameTaken, + StatusCode::BAD_REQUEST, + ) + .await; // 3. sigining in with non-existent user - let nonexistantuser = Login { + let mut login = Login { username: "nonexistantuser".into(), password: msg.password.clone(), }; - let userdoesntexist = test::call_service( - &mut app, - post_request!(&nonexistantuser, "/api/v1/signin").to_request(), + bad_post_req_test( + NAME, + PASSWORD, + SIGNIN, + &login, + ServiceError::UsernameNotFound, + StatusCode::UNAUTHORIZED, ) .await; - assert_eq!(userdoesntexist.status(), StatusCode::UNAUTHORIZED); - let txt: ErrorToResponse = test::read_body_json(userdoesntexist).await; - assert_eq!(txt.error, format!("{}", ServiceError::UsernameNotFound)); // 4. trying to signin with wrong password - let wrongpassword = Login { - username: NAME.into(), - password: NAME.into(), - }; - let wrongpassword_resp = test::call_service( - &mut app, - post_request!(&wrongpassword, "/api/v1/signin").to_request(), + login.username = NAME.into(); + login.password = NAME.into(); + + bad_post_req_test( + NAME, + PASSWORD, + SIGNIN, + &login, + ServiceError::WrongPassword, + StatusCode::UNAUTHORIZED, ) .await; - assert_eq!(wrongpassword_resp.status(), StatusCode::UNAUTHORIZED); - let txt: ErrorToResponse = test::read_body_json(wrongpassword_resp).await; - assert_eq!(txt.error, format!("{}", ServiceError::WrongPassword)); // 5. signout let signout_resp = test::call_service( &mut app, - post_request!(&wrongpassword, "/api/v1/signout") - .cookie(cookies.clone()) + test::TestRequest::post() + .uri("/api/v1/signout") + .cookie(cookies) .to_request(), ) .await; assert_eq!(signout_resp.status(), StatusCode::OK); - - delete_user(NAME, &data).await; } #[actix_rt::test] @@ -238,7 +246,12 @@ mod tests { const PASSWORD: &str = "longpassword2"; const EMAIL: &str = "testuser1@a.com2"; - let (data, creds, signin_resp) = signin_util(NAME, EMAIL, PASSWORD).await; + { + 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; @@ -251,6 +264,5 @@ mod tests { .await; assert_eq!(delete_user_resp.status(), StatusCode::OK); - delete_user(NAME, &data).await; } } diff --git a/src/api/v1/mcaptcha.rs b/src/api/v1/mcaptcha.rs index 282b08e4..2059cd1d 100644 --- a/src/api/v1/mcaptcha.rs +++ b/src/api/v1/mcaptcha.rs @@ -39,15 +39,18 @@ pub async fn add_domain( let url = Url::parse(&payload.name)?; if let Some(host) = url.host_str() { let user = id.identity().unwrap(); - sqlx::query!( + let res = sqlx::query!( "insert into mcaptcha_domains (name, ID) values ($1, (select ID from mcaptcha_users where name = ($2) ));", host, user ) .execute(&data.db) - .await?; - Ok(HttpResponse::Ok()) + .await; + match res { + Err(e) => Err(dup_error(e, ServiceError::HostnameTaken)), + Ok(_) => Ok(HttpResponse::Ok()), + } } else { Err(ServiceError::NotAUrl) } @@ -72,8 +75,9 @@ pub async fn delete_domain( } #[derive(Clone, Debug, Deserialize, Serialize)] -pub struct TokenName { +pub struct CreateToken { pub name: String, + pub domain: String, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -82,34 +86,60 @@ pub struct TokenKeyPair { pub key: String, } -//#[post("/api/v1/mcaptcha/domain/token/add")] -//pub async fn add_mcaptcha( -// payload: web::Json, -// data: web::Data, -// id: Identity, -//) -> ServiceResult { -// is_authenticated(&id)?; -// let key = get_random(32); -// let res = sqlx::query!( -// "INSERT INTO mcaptcha_config (name, key) VALUES ($1, $2)", -// &payload.name, -// &key, -// ) -// .execute(&data.db) -// .await; -// -// match res { -// Err(e) => Err(dup_error(e, ServiceError::UsernameTaken)), -// Ok(_) => { -// let resp = TokenKeyPair { -// key, -// name: payload.name, -// }; -// -// Ok(HttpResponse::Ok().json(resp)) -// } -// } -//} +#[post("/api/v1/mcaptcha/domain/token/add")] +pub async fn add_mcaptcha( + payload: web::Json, + data: web::Data, + id: Identity, +) -> ServiceResult { + is_authenticated(&id)?; + let key = get_random(32); + let url = Url::parse(&payload.domain)?; + println!("got req"); + if let Some(host) = url.host_str() { + let res = sqlx::query!( + "INSERT INTO mcaptcha_config + (name, key, domain_name) + VALUES ($1, $2, ( + SELECT name FROM mcaptcha_domains WHERE name = ($3)))", + &payload.name, + &key, + &host, + ) + .execute(&data.db) + .await; + + match res { + Err(e) => Err(dup_error(e, ServiceError::TokenNameTaken)), + Ok(_) => { + let resp = TokenKeyPair { + key, + name: payload.into_inner().name, + }; + + Ok(HttpResponse::Ok().json(resp)) + } + } + } else { + Err(ServiceError::NotAUrl) + } +} + +#[post("/api/v1/mcaptcha/domain/token/delete")] +pub async fn delete_mcaptcha( + payload: web::Json, + data: web::Data, + id: Identity, +) -> ServiceResult { + is_authenticated(&id)?; + sqlx::query!( + "DELETE FROM mcaptcha_config WHERE name = ($1)", + &payload.name, + ) + .execute(&data.db) + .await?; + Ok(HttpResponse::Ok()) +} fn get_random(len: usize) -> String { use std::iter; @@ -141,28 +171,36 @@ mod tests { 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, _, signin_resp) = signin_util(NAME, EMAIL, PASSWORD).await; + { + 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; - delete_domain_util(DOMAIN, &data).await; - - // 1. add domain - let domain = Domain { + let mut domain = Domain { name: DOMAIN.into(), }; - let add_domain_resp = test::call_service( - &mut app, - post_request!(&domain, "/api/v1/mcaptcha/domain/add") - .cookie(cookies.clone()) - .to_request(), + // 2. duplicate domain + bad_post_req_test( + NAME, + PASSWORD, + ADD_URL, + &domain, + ServiceError::HostnameTaken, + StatusCode::BAD_REQUEST, ) .await; - assert_eq!(add_domain_resp.status(), StatusCode::OK); - // 2. delete domain + // 3. delete domain let del_domain_resp = test::call_service( &mut app, post_request!(&domain, "/api/v1/mcaptcha/domain/delete") @@ -171,6 +209,85 @@ mod tests { ) .await; assert_eq!(del_domain_resp.status(), StatusCode::OK); - delete_user(NAME, &data).await; + + // 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; + } + + 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(), + ) + .await; + assert_eq!(add_token_resp.status(), StatusCode::OK); + + // 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); } } diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index bc06b77b..d83f2aa4 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -31,4 +31,6 @@ pub fn services(cfg: &mut ServiceConfig) { cfg.service(add_domain); cfg.service(delete_domain); + cfg.service(add_mcaptcha); + cfg.service(delete_mcaptcha); } diff --git a/src/errors.rs b/src/errors.rs index 8bfef08e..c121df09 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -76,6 +76,9 @@ pub enum ServiceError { /// 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, } #[derive(Serialize, Deserialize)] @@ -97,6 +100,7 @@ impl ResponseError for ServiceError { #[cfg(not(tarpaulin_include))] fn status_code(&self) -> StatusCode { + println!("{:?}", &self); match *self { ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, ServiceError::NotAnEmail => StatusCode::BAD_REQUEST, @@ -111,6 +115,7 @@ impl ResponseError for ServiceError { ServiceError::UsernameCaseMappedError => StatusCode::BAD_REQUEST, ServiceError::UsernameTaken => StatusCode::BAD_REQUEST, ServiceError::TokenNameTaken => StatusCode::BAD_REQUEST, + ServiceError::HostnameTaken => StatusCode::BAD_REQUEST, } } } @@ -162,6 +167,7 @@ impl From for ServiceError { pub fn dup_error(e: sqlx::Error, dup_error: ServiceError) -> ServiceError { use sqlx::error::Error; use std::borrow::Cow; + println!("database error: {:?}", &e); if let Error::Database(err) = e { if err.code() == Some(Cow::from("23505")) { dup_error diff --git a/src/tests/mod.rs b/src/tests/mod.rs index aea9c144..b3ca2f96 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -3,11 +3,13 @@ use actix_web::{ dev::ServiceResponse, http::{header, StatusCode}, }; +use serde::Serialize; use super::*; use crate::api::v1::auth::{Login, Register}; use crate::api::v1::services as v1_services; use crate::data::Data; +use crate::errors::*; #[macro_export] macro_rules! get_cookie { @@ -17,15 +19,13 @@ macro_rules! get_cookie { } pub async fn delete_user(name: &str, data: &Data) { - let _ = sqlx::query!("DELETE FROM mcaptcha_users WHERE name = ($1)", name,) - .execute(&data.db) - .await; -} - -pub async fn delete_domain_util(name: &str, data: &Data) { - let _ = sqlx::query!("DELETE FROM mcaptcha_domains WHERE name = ($1)", name,) + let r = sqlx::query!("DELETE FROM mcaptcha_users WHERE name = ($1)", name,) .execute(&data.db) .await; + println!(); + println!(); + println!(); + println!("Deleting user: {:?}", &r); } #[macro_export] @@ -51,16 +51,20 @@ macro_rules! get_app { } /// register and signin utility -pub async fn signin_util<'a>( +pub async fn register_and_signin<'a>( name: &'a str, email: &str, password: &str, ) -> (data::Data, Login, ServiceResponse) { + register(name, email, password).await; + signin(name, password).await +} + +/// register and signin 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; - delete_user(&name, &data).await; - // 1. Register let msg = Register { username: name.into(), @@ -70,6 +74,12 @@ pub async fn signin_util<'a>( let resp = test::call_service(&mut app, post_request!(&msg, "/api/v1/signup").to_request()).await; assert_eq!(resp.status(), StatusCode::OK); +} + +/// signin util +pub async fn signin<'a>(name: &'a str, password: &str) -> (data::Data, Login, ServiceResponse) { + let data = Data::new().await; + let mut app = get_app!(data).await; // 2. signin let creds = Login { @@ -85,3 +95,57 @@ pub async fn signin_util<'a>( (data, creds, signin_resp) } + +/// register and signin and domain +pub async fn add_domain_util( + name: &str, + password: &str, + domain: &str, +) -> (data::Data, Login, ServiceResponse) { + use crate::api::v1::mcaptcha::Domain; + + let (data, creds, signin_resp) = signin(name, password).await; + let cookies = get_cookie!(signin_resp); + let mut app = get_app!(data).await; + + // 1. add domain + let domain = Domain { + name: domain.into(), + }; + + let add_domain_resp = test::call_service( + &mut app, + post_request!(&domain, "/api/v1/mcaptcha/domain/add") + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(add_domain_resp.status(), StatusCode::OK); + + (data, creds, signin_resp) +} + +/// pub duplicate test +pub async fn bad_post_req_test( + name: &str, + password: &str, + url: &str, + payload: &T, + dup_err: ServiceError, + s: StatusCode, +) { + let (data, _, signin_resp) = signin(name, password).await; + let cookies = get_cookie!(signin_resp); + let mut app = get_app!(data).await; + + let dup_token_resp = test::call_service( + &mut app, + post_request!(&payload, url) + .cookie(cookies.clone()) + .to_request(), + ) + .await; + assert_eq!(dup_token_resp.status(), s); + let txt: ErrorToResponse = test::read_body_json(dup_token_resp).await; + assert_eq!(txt.error, format!("{}", dup_err)); +}