cascading delete, mcaptcha add/del and test suite

This commit is contained in:
realaravinth 2021-03-11 20:58:18 +05:30
parent 6be10af6fd
commit 096dcd32e4
No known key found for this signature in database
GPG key ID: AD9F0F08E855ED88
8 changed files with 287 additions and 85 deletions

View file

@ -1,4 +1,4 @@
CREATE TABLE IF NOT EXISTS mcaptcha_domains ( CREATE TABLE IF NOT EXISTS mcaptcha_domains (
name VARCHAR(100) PRIMARY KEY NOT NULL UNIQUE, 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
); );

View file

@ -1,6 +1,7 @@
CREATE TABLE IF NOT EXISTS mcaptcha_config ( CREATE TABLE IF NOT EXISTS mcaptcha_config (
config_id SERIAL PRIMARY KEY NOT NULL, 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, key VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL UNIQUE,
duration INTEGER NOT NULL DEFAULT 30 duration INTEGER NOT NULL DEFAULT 30
); );

View file

@ -1,5 +1,5 @@
CREATE TABLE IF NOT EXISTS mcaptcha_levels ( 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, difficulty_factor INTEGER NOT NULL,
visitor_threshold INTEGER NOT NULL, visitor_threshold INTEGER NOT NULL,
level_id SERIAL PRIMARY KEY NOT NULL level_id SERIAL PRIMARY KEY NOT NULL

View file

@ -172,13 +172,15 @@ mod tests {
const NAME: &str = "testuser"; const NAME: &str = "testuser";
const PASSWORD: &str = "longpassword"; const PASSWORD: &str = "longpassword";
const EMAIL: &str = "testuser1@a.com"; 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; let mut app = get_app!(data).await;
delete_user(NAME, &data).await; delete_user(NAME, &data).await;
// 1. Register and signin // 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); let cookies = get_cookie!(signin_resp);
// 2. check if duplicate username is allowed // 2. check if duplicate username is allowed
@ -187,49 +189,55 @@ mod tests {
password: PASSWORD.into(), password: PASSWORD.into(),
email: EMAIL.into(), email: EMAIL.into(),
}; };
let duplicate_user_resp = bad_post_req_test(
test::call_service(&mut app, post_request!(&msg, "/api/v1/signup").to_request()).await; NAME,
assert_eq!(duplicate_user_resp.status(), StatusCode::BAD_REQUEST); PASSWORD,
SIGNUP,
&msg,
ServiceError::UsernameTaken,
StatusCode::BAD_REQUEST,
)
.await;
// 3. sigining in with non-existent user // 3. sigining in with non-existent user
let nonexistantuser = Login { let mut login = Login {
username: "nonexistantuser".into(), username: "nonexistantuser".into(),
password: msg.password.clone(), password: msg.password.clone(),
}; };
let userdoesntexist = test::call_service( bad_post_req_test(
&mut app, NAME,
post_request!(&nonexistantuser, "/api/v1/signin").to_request(), PASSWORD,
SIGNIN,
&login,
ServiceError::UsernameNotFound,
StatusCode::UNAUTHORIZED,
) )
.await; .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 // 4. trying to signin with wrong password
let wrongpassword = Login { login.username = NAME.into();
username: NAME.into(), login.password = NAME.into();
password: NAME.into(),
}; bad_post_req_test(
let wrongpassword_resp = test::call_service( NAME,
&mut app, PASSWORD,
post_request!(&wrongpassword, "/api/v1/signin").to_request(), SIGNIN,
&login,
ServiceError::WrongPassword,
StatusCode::UNAUTHORIZED,
) )
.await; .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 // 5. signout
let signout_resp = test::call_service( let signout_resp = test::call_service(
&mut app, &mut app,
post_request!(&wrongpassword, "/api/v1/signout") test::TestRequest::post()
.cookie(cookies.clone()) .uri("/api/v1/signout")
.cookie(cookies)
.to_request(), .to_request(),
) )
.await; .await;
assert_eq!(signout_resp.status(), StatusCode::OK); assert_eq!(signout_resp.status(), StatusCode::OK);
delete_user(NAME, &data).await;
} }
#[actix_rt::test] #[actix_rt::test]
@ -238,7 +246,12 @@ mod tests {
const PASSWORD: &str = "longpassword2"; const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "testuser1@a.com2"; 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 cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await; let mut app = get_app!(data).await;
@ -251,6 +264,5 @@ mod tests {
.await; .await;
assert_eq!(delete_user_resp.status(), StatusCode::OK); assert_eq!(delete_user_resp.status(), StatusCode::OK);
delete_user(NAME, &data).await;
} }
} }

View file

@ -39,15 +39,18 @@ pub async fn add_domain(
let url = Url::parse(&payload.name)?; let url = Url::parse(&payload.name)?;
if let Some(host) = url.host_str() { if let Some(host) = url.host_str() {
let user = id.identity().unwrap(); let user = id.identity().unwrap();
sqlx::query!( let res = sqlx::query!(
"insert into mcaptcha_domains (name, ID) values "insert into mcaptcha_domains (name, ID) values
($1, (select ID from mcaptcha_users where name = ($2) ));", ($1, (select ID from mcaptcha_users where name = ($2) ));",
host, host,
user user
) )
.execute(&data.db) .execute(&data.db)
.await?; .await;
Ok(HttpResponse::Ok()) match res {
Err(e) => Err(dup_error(e, ServiceError::HostnameTaken)),
Ok(_) => Ok(HttpResponse::Ok()),
}
} else { } else {
Err(ServiceError::NotAUrl) Err(ServiceError::NotAUrl)
} }
@ -72,8 +75,9 @@ pub async fn delete_domain(
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TokenName { pub struct CreateToken {
pub name: String, pub name: String,
pub domain: String,
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
@ -82,34 +86,60 @@ pub struct TokenKeyPair {
pub key: String, pub key: String,
} }
//#[post("/api/v1/mcaptcha/domain/token/add")] #[post("/api/v1/mcaptcha/domain/token/add")]
//pub async fn add_mcaptcha( pub async fn add_mcaptcha(
// payload: web::Json<Domain>, payload: web::Json<CreateToken>,
// data: web::Data<Data>, data: web::Data<Data>,
// id: Identity, id: Identity,
//) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
// is_authenticated(&id)?; is_authenticated(&id)?;
// let key = get_random(32); let key = get_random(32);
// let res = sqlx::query!( let url = Url::parse(&payload.domain)?;
// "INSERT INTO mcaptcha_config (name, key) VALUES ($1, $2)", println!("got req");
// &payload.name, if let Some(host) = url.host_str() {
// &key, let res = sqlx::query!(
// ) "INSERT INTO mcaptcha_config
// .execute(&data.db) (name, key, domain_name)
// .await; VALUES ($1, $2, (
// SELECT name FROM mcaptcha_domains WHERE name = ($3)))",
// match res { &payload.name,
// Err(e) => Err(dup_error(e, ServiceError::UsernameTaken)), &key,
// Ok(_) => { &host,
// let resp = TokenKeyPair { )
// key, .execute(&data.db)
// name: payload.name, .await;
// };
// match res {
// Ok(HttpResponse::Ok().json(resp)) 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<CreateToken>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
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 { fn get_random(len: usize) -> String {
use std::iter; use std::iter;
@ -141,28 +171,36 @@ mod tests {
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testuserdomain@a.com"; const EMAIL: &str = "testuserdomain@a.com";
const DOMAIN: &str = "http://example.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 cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await; let mut app = get_app!(data).await;
delete_domain_util(DOMAIN, &data).await; let mut domain = Domain {
// 1. add domain
let domain = Domain {
name: DOMAIN.into(), name: DOMAIN.into(),
}; };
let add_domain_resp = test::call_service( // 2. duplicate domain
&mut app, bad_post_req_test(
post_request!(&domain, "/api/v1/mcaptcha/domain/add") NAME,
.cookie(cookies.clone()) PASSWORD,
.to_request(), ADD_URL,
&domain,
ServiceError::HostnameTaken,
StatusCode::BAD_REQUEST,
) )
.await; .await;
assert_eq!(add_domain_resp.status(), StatusCode::OK);
// 2. delete domain // 3. delete domain
let del_domain_resp = test::call_service( let del_domain_resp = test::call_service(
&mut app, &mut app,
post_request!(&domain, "/api/v1/mcaptcha/domain/delete") post_request!(&domain, "/api/v1/mcaptcha/domain/delete")
@ -171,6 +209,85 @@ mod tests {
) )
.await; .await;
assert_eq!(del_domain_resp.status(), StatusCode::OK); 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);
} }
} }

View file

@ -31,4 +31,6 @@ pub fn services(cfg: &mut ServiceConfig) {
cfg.service(add_domain); cfg.service(add_domain);
cfg.service(delete_domain); cfg.service(delete_domain);
cfg.service(add_mcaptcha);
cfg.service(delete_mcaptcha);
} }

View file

@ -76,6 +76,9 @@ pub enum ServiceError {
/// when the a token name is already taken /// when the a token name is already taken
#[display(fmt = "token name not available")] #[display(fmt = "token name not available")]
TokenNameTaken, TokenNameTaken,
/// when the a host name is already taken
#[display(fmt = "host name not available")]
HostnameTaken,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -97,6 +100,7 @@ impl ResponseError for ServiceError {
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
println!("{:?}", &self);
match *self { match *self {
ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
ServiceError::NotAnEmail => StatusCode::BAD_REQUEST, ServiceError::NotAnEmail => StatusCode::BAD_REQUEST,
@ -111,6 +115,7 @@ impl ResponseError for ServiceError {
ServiceError::UsernameCaseMappedError => StatusCode::BAD_REQUEST, ServiceError::UsernameCaseMappedError => StatusCode::BAD_REQUEST,
ServiceError::UsernameTaken => StatusCode::BAD_REQUEST, ServiceError::UsernameTaken => StatusCode::BAD_REQUEST,
ServiceError::TokenNameTaken => StatusCode::BAD_REQUEST, ServiceError::TokenNameTaken => StatusCode::BAD_REQUEST,
ServiceError::HostnameTaken => StatusCode::BAD_REQUEST,
} }
} }
} }
@ -162,6 +167,7 @@ impl From<sqlx::Error> for ServiceError {
pub fn dup_error(e: sqlx::Error, dup_error: ServiceError) -> ServiceError { pub fn dup_error(e: sqlx::Error, dup_error: ServiceError) -> ServiceError {
use sqlx::error::Error; use sqlx::error::Error;
use std::borrow::Cow; use std::borrow::Cow;
println!("database error: {:?}", &e);
if let Error::Database(err) = e { if let Error::Database(err) = e {
if err.code() == Some(Cow::from("23505")) { if err.code() == Some(Cow::from("23505")) {
dup_error dup_error

View file

@ -3,11 +3,13 @@ use actix_web::{
dev::ServiceResponse, dev::ServiceResponse,
http::{header, StatusCode}, http::{header, StatusCode},
}; };
use serde::Serialize;
use super::*; use super::*;
use crate::api::v1::auth::{Login, Register}; use crate::api::v1::auth::{Login, Register};
use crate::api::v1::services as v1_services; use crate::api::v1::services as v1_services;
use crate::data::Data; use crate::data::Data;
use crate::errors::*;
#[macro_export] #[macro_export]
macro_rules! get_cookie { macro_rules! get_cookie {
@ -17,15 +19,13 @@ macro_rules! get_cookie {
} }
pub async fn delete_user(name: &str, data: &Data) { pub async fn delete_user(name: &str, data: &Data) {
let _ = sqlx::query!("DELETE FROM mcaptcha_users WHERE name = ($1)", name,) let r = 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,)
.execute(&data.db) .execute(&data.db)
.await; .await;
println!();
println!();
println!();
println!("Deleting user: {:?}", &r);
} }
#[macro_export] #[macro_export]
@ -51,16 +51,20 @@ macro_rules! get_app {
} }
/// register and signin utility /// register and signin utility
pub async fn signin_util<'a>( pub async fn register_and_signin<'a>(
name: &'a str, name: &'a str,
email: &str, email: &str,
password: &str, password: &str,
) -> (data::Data, Login, ServiceResponse) { ) -> (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 data = Data::new().await;
let mut app = get_app!(data).await; let mut app = get_app!(data).await;
delete_user(&name, &data).await;
// 1. Register // 1. Register
let msg = Register { let msg = Register {
username: name.into(), username: name.into(),
@ -70,6 +74,12 @@ pub async fn signin_util<'a>(
let resp = let resp =
test::call_service(&mut app, post_request!(&msg, "/api/v1/signup").to_request()).await; test::call_service(&mut app, post_request!(&msg, "/api/v1/signup").to_request()).await;
assert_eq!(resp.status(), StatusCode::OK); 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 // 2. signin
let creds = Login { let creds = Login {
@ -85,3 +95,57 @@ pub async fn signin_util<'a>(
(data, creds, signin_resp) (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<T: Serialize>(
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));
}