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 (
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 (
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
);

View file

@ -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

View file

@ -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;
}
}

View file

@ -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<Domain>,
// data: web::Data<Data>,
// id: Identity,
//) -> ServiceResult<impl Responder> {
// 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<CreateToken>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
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<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 {
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);
}
}

View file

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

View file

@ -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<sqlx::Error> 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

View file

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