Add integration tests

This commit is contained in:
Rohan Sircar 2021-05-05 20:00:11 +05:30
parent eb9e97c3aa
commit 26b76e4226
12 changed files with 335 additions and 195 deletions

9
.env
View File

@ -1,3 +1,8 @@
#this is required for diesel_cli to work
DATABASE_URL=data/app.db
BIND_ADDRESS=127.0.0.1:7800
HASH_COST=8
ACTIX_DEMO_DATABASE_URL=${DATABASE_URL}
ACTIX_DEMO_RUST_LOG=actix_demo=debug
ACTIX_DEMO_TEST_RUST_LOG=debug
ACTIX_DEMO_HTTP_HOST=127.0.0.1
ACTIX_DEMO_HASH_COST=8

View File

@ -38,11 +38,17 @@ jobs:
override: true
- name: Rust Cache
uses: Swatinem/rust-cache@v1.2.0
- name: Run Tests
- name: Run Unit Tests
uses: actions-rs/cargo@v1
with:
command: test
args: --lib
- name: Run Integration Tests
uses: actions-rs/cargo@v1
with:
command: test
args: --test integration
env: ACTIX_DEMO_TEST_RUST_LOG=warn
lints:
name: Lints
@ -99,7 +105,7 @@ jobs:
use-cross: true
command: build
args: --release --target=aarch64-unknown-linux-gnu
build-ppc:
name: Build PowerPC Binaries
if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
@ -132,7 +138,7 @@ jobs:
with:
command: build
args: --release --target=powerpc64-unknown-linux-gnu
env:
env:
RUSTFLAGS: "-C linker=powerpc64-linux-gnu-gcc-7"
publish-docker:
@ -161,7 +167,7 @@ jobs:
args: --release
- name: Build Image
run: docker build -f ci.Dockerfile -t rohansircar/actix-demo:latest .
env:
env:
DOCKER_BUILDKIT: 1
- name: Publish Image
run: docker push rohansircar/actix-demo:latest

49
Cargo.lock generated
View File

@ -40,7 +40,9 @@ name = "actix-demo"
version = "0.1.0"
dependencies = [
"actix-files",
"actix-http",
"actix-identity",
"actix-rt",
"actix-service 2.0.0",
"actix-threadpool",
"actix-web",
@ -48,18 +50,16 @@ dependencies = [
"bcrypt",
"bytes 1.0.1",
"chrono",
"comp",
"custom_error",
"derive-new",
"diesel",
"diesel_migrations",
"dotenv",
"env_logger",
"envy",
"futures",
"json",
"lazy-regex",
"lazy_static",
"listenfd",
"log",
"nanoid",
"r2d2",
@ -69,7 +69,7 @@ dependencies = [
"serde",
"serde_json",
"timeago",
"uuid 0.8.2",
"uuid",
"validator",
"validator_derive",
]
@ -644,12 +644,6 @@ dependencies = [
"bitflags",
]
[[package]]
name = "comp"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7b6cae29f71a26f0dae0e291da438d6fced0e22e78aa1484cbbc085b5170949"
[[package]]
name = "const_fn"
version = "0.4.7"
@ -849,6 +843,15 @@ dependencies = [
"termcolor",
]
[[package]]
name = "envy"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965"
dependencies = [
"serde",
]
[[package]]
name = "fallible-iterator"
version = "0.2.0"
@ -1225,12 +1228,6 @@ version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "json"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd"
[[package]]
name = "kernel32-sys"
version = "0.2.2"
@ -1285,17 +1282,6 @@ version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3"
[[package]]
name = "listenfd"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "492158e732f2e2de81c592f0a2427e57e12cd3d59877378fe7af624b6bbe0ca1"
dependencies = [
"libc",
"uuid 0.6.5",
"winapi 0.3.9",
]
[[package]]
name = "lock_api"
version = "0.4.3"
@ -2486,15 +2472,6 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "uuid"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1436e58182935dcd9ce0add9ea0b558e8a87befe01c1a301e6020aeb0876363"
dependencies = [
"cfg-if 0.1.10",
]
[[package]]
name = "uuid"
version = "0.8.2"

View File

@ -6,16 +6,16 @@ edition = '2018'
[dependencies]
actix-web = "3.3.2"
# actix-rt = "1.1.1"
actix-service = "2.0.0"
actix-files = "0.5.0"
actix-http = "2.2.0"
bytes = "1.0.1"
futures = "0.3.14"
log = "0.4.14"
env_logger = "0.8.3"
serde_json = "1.0.64"
json = "0.12.4"
listenfd = "0.3.3"
# json = "0.12.4"
# listenfd = "0.3.3"
dotenv = "0.15.0"
r2d2 = "0.8.9"
validator = "0.13.0"
@ -27,7 +27,7 @@ rand = "0.8.3"
nanoid = "0.4.0"
bcrypt = "0.9.0"
timeago = "0.3.0"
comp = "0.2.1"
# comp = "0.2.1"
regex = "1.4.5"
lazy_static = "1.4.0"
lazy-regex = "0.1.4"
@ -35,29 +35,19 @@ custom_error = "1.9.2"
derive-new = "0.5.9"
diesel_migrations = "1.4.0"
actix-threadpool = "0.3.3"
envy = "0.4"
[dependencies.serde]
version = "1.0.125"
features = ['derive']
# [dependencies.yarte]
# version = '0.9.0'
# features = ['html-min']
[dependencies.diesel]
version = "1.4.5"
features = [
'sqlite',
'r2d2',
'chrono',
]
features = ['sqlite', 'r2d2', 'chrono']
[dependencies.uuid]
version = "0.8.2"
features = [
'serde',
'v4',
]
features = ['serde', 'v4']
[dependencies.rusqlite]
version = "0.23.1"
@ -66,7 +56,6 @@ features = ['bundled']
[dependencies.chrono]
version = "0.4.19"
features = ['serde']
# [build-dependencies.yarte_helpers]
# version = '0.9.0'
# default-features = false
# features = ['config']
[dev-dependencies]
actix-rt = "1.1.1"

View File

@ -40,7 +40,7 @@ impl ResponseError for DomainError {
HttpResponse::InternalServerError().json(ErrorModel {
// error_code: 500,
success: false,
reason: err.to_string().as_str(),
reason: err.to_string(),
})
}
DomainError::DbError { source: _ } => {
@ -48,7 +48,7 @@ impl ResponseError for DomainError {
HttpResponse::InternalServerError().json(ErrorModel {
// error_code: 500,
success: false,
reason: "Error in database",
reason: "Error in database".to_owned(),
})
}
DomainError::DbPoolError { source: _ } => {
@ -56,21 +56,21 @@ impl ResponseError for DomainError {
HttpResponse::InternalServerError().json(ErrorModel {
// error_code: 500,
success: false,
reason: "Error getting database pool",
reason: "Error getting database pool".to_owned(),
})
}
DomainError::PasswordError { cause: _ } => {
HttpResponse::BadRequest().json(ErrorModel {
// error_code: 400,
success: false,
reason: err.to_string().as_str(),
reason: err.to_string(),
})
}
DomainError::EntityDoesNotExistError { message: _ } => {
HttpResponse::Accepted().json(ErrorModel {
// error_code: 400,
success: false,
reason: err.to_string().as_str(),
reason: err.to_string(),
})
}
DomainError::ThreadPoolError { message: _ } => {
@ -78,14 +78,14 @@ impl ResponseError for DomainError {
HttpResponse::InternalServerError().json(ErrorModel {
// error_code: 400,
success: false,
reason: "Thread pool error occurred",
reason: "Thread pool error occurred".to_owned(),
})
}
DomainError::AuthError { message: _ } => {
HttpResponse::Accepted().json(ErrorModel {
// error_code: 400,
success: false,
reason: err.to_string().as_str(),
reason: err.to_string(),
})
}
}

96
src/lib.rs Normal file
View File

@ -0,0 +1,96 @@
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate derive_new;
#[macro_use]
extern crate log;
extern crate bcrypt;
extern crate custom_error;
extern crate regex;
extern crate validator;
mod actions;
mod errors;
mod middlewares;
pub mod models;
mod routes;
mod schema;
mod services;
mod types;
mod utils;
use actix_files as fs;
use actix_identity::{CookieIdentityPolicy, IdentityService};
use actix_web::{cookie::SameSite, middleware, web, App, HttpServer};
use actix_web::{middleware::Logger, web::ServiceConfig};
use rand::Rng;
use serde::Deserialize;
use types::DbPool;
#[derive(Deserialize, Debug, Clone)]
pub struct EnvConfig {
pub database_url: String,
pub http_host: String,
#[serde(default = "default_hash_cost")]
pub hash_cost: u8,
}
#[derive(Deserialize, Debug, Clone)]
pub struct AppConfig {
pub hash_cost: u8,
}
#[derive(Clone)]
pub struct AppData {
pub config: AppConfig,
pub pool: DbPool,
}
pub fn default_hash_cost() -> u8 {
8
}
pub fn configure_app(app_data: AppData) -> Box<dyn Fn(&mut ServiceConfig)> {
Box::new(move |cfg: &mut ServiceConfig| {
cfg.data(app_data.clone())
.service(
web::scope("/api")
.service(routes::users::get_user)
.service(routes::users::get_all_users),
)
// .route("/api/users/get", web::get().to(user_controller.get_user.into()))
.service(web::scope("/api/public")) // public endpoint - not implemented yet
.service(routes::auth::login)
.service(routes::auth::logout)
.service(routes::auth::index)
.service(routes::users::add_user)
.service(fs::Files::new("/", "./static"));
})
}
pub fn id_service(
private_key: &[u8],
) -> actix_identity::IdentityService<CookieIdentityPolicy> {
IdentityService::new(
CookieIdentityPolicy::new(&private_key)
.name("my-app-auth")
.secure(false)
.same_site(SameSite::Lax),
)
}
pub fn app_logger() -> Logger {
middleware::Logger::default()
}
pub async fn run(addr: String, app_data: AppData) -> std::io::Result<()> {
info!("Starting server at {}", addr);
let private_key = rand::thread_rng().gen::<[u8; 32]>();
let app = move || {
App::new()
.configure(configure_app(app_data.clone()))
.wrap(id_service(&private_key))
.wrap(app_logger())
};
HttpServer::new(app).bind(addr)?.run().await
}

View File

@ -1,64 +1,38 @@
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate derive_new;
extern crate bcrypt;
extern crate custom_error;
extern crate regex;
extern crate validator;
use actix_web::{cookie::SameSite, middleware, web, App, HttpServer};
use actix_files as fs;
use actix_identity::{CookieIdentityPolicy, IdentityService};
use rand::Rng;
use diesel::prelude::*;
use diesel::r2d2::{self, ConnectionManager};
use actix_demo::{AppConfig, AppData, EnvConfig};
use diesel::{r2d2::ConnectionManager, SqliteConnection};
use env_logger::Env;
use io::ErrorKind;
use std::io;
use std::io::ErrorKind;
use types::DbPool;
mod actions;
mod errors;
mod middlewares;
mod models;
mod routes;
mod schema;
mod services;
mod types;
mod utils;
#[macro_use]
extern crate log;
#[derive(Clone)]
pub struct AppConfig {
hash_cost: u32,
pool: DbPool,
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "debug");
env_logger::init();
dotenv::dotenv().map_err(|err| {
async fn main() -> io::Result<()> {
let _ = dotenv::dotenv().map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to set up env: {:?}", err),
)
})?;
// let _basic_auth_middleware =
// HttpAuthentication::basic(utils::auth::validator);
// set up database connection pool
let connspec = std::env::var("DATABASE_URL").map_err(|err| {
let _ = env_logger::try_init_from_env(
Env::default().filter("ACTIX_DEMO_RUST_LOG"),
)
.map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Database url is not set: {:?}", err),
format!("Failed to set up env logger: {:?}", err),
)
})?;
let env_config = envy::prefixed("ACTIX_DEMO_")
.from_env::<EnvConfig>()
.map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to parse config: {:?}", err),
)
})?;
let connspec = &env_config.database_url;
let manager = ConnectionManager::<SqliteConnection>::new(connspec);
let pool = r2d2::Pool::builder().build(manager).map_err(|err| {
io::Error::new(
@ -67,7 +41,7 @@ async fn main() -> std::io::Result<()> {
)
})?;
{
let _ = {
let conn = &pool.get().map_err(|err| {
io::Error::new(
ErrorKind::Other,
@ -75,60 +49,21 @@ async fn main() -> std::io::Result<()> {
)
})?;
diesel_migrations::run_pending_migrations(conn).map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Error running migrations: {:?}", err),
)
})?;
}
let _ =
diesel_migrations::run_pending_migrations(conn).map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Error running migrations: {:?}", err),
)
})?;
};
let hash_cost = std::env::var("HASH_COST")
.map_err(|e| e.to_string())
.and_then(|x| x.parse::<u32>().map_err(|e| e.to_string()))
.unwrap_or_else(|err| {
info!(
"Error getting hash cost: {:?}. Using default cost of 8",
err
);
8
});
let config: AppConfig = AppConfig {
let app_data = AppData {
config: AppConfig {
hash_cost: env_config.hash_cost,
},
pool: pool.clone(),
hash_cost,
};
// let user_controller = UserController {
// user_service: &user_service,
// };
let addr = std::env::var("BIND_ADDRESS")
.unwrap_or_else(|_| "127.0.0.1:7800".to_owned());
info!("Starting server at {}", addr);
let private_key = rand::thread_rng().gen::<[u8; 32]>();
let app = move || {
App::new()
.data(config.clone())
.wrap(IdentityService::new(
CookieIdentityPolicy::new(&private_key)
.name("my-app-auth")
.secure(false)
.same_site(SameSite::Lax),
))
.wrap(middleware::Logger::default())
.service(
web::scope("/api")
.service(routes::users::get_user)
.service(routes::users::get_all_users),
)
// .route("/api/users/get", web::get().to(user_controller.get_user.into()))
.service(web::scope("/api/public")) // public endpoint - not implemented yet
.service(routes::auth::login)
.service(routes::auth::logout)
.service(routes::auth::index)
.service(routes::users::add_user)
.service(fs::Files::new("/", "./static"))
};
HttpServer::new(app).bind(addr)?.run().await
actix_demo::run(format!("{}:7800", env_config.http_host), app_data).await
}

View File

@ -1,4 +1,4 @@
use serde::Serialize;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, new)]
pub struct JsonErrorModel<'a> {
@ -6,9 +6,9 @@ pub struct JsonErrorModel<'a> {
pub line: String,
pub reason: &'a str,
}
#[derive(Debug, Clone, Serialize, new)]
pub struct ErrorModel<'a> {
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize, new)]
pub struct ErrorModel {
// pub error_code: i16,
pub success: bool,
pub reason: &'a str,
pub reason: String,
}

View File

@ -1,8 +1,8 @@
use actix_web::web;
use actix_web_httpauth::extractors::basic::BasicAuth;
use crate::actions::users;
use crate::{errors::DomainError, AppConfig};
use crate::errors::DomainError;
use crate::{actions::users, AppData};
use actix_identity::Identity;
use actix_web::{get, Error, HttpResponse};
@ -10,7 +10,7 @@ use actix_web::{get, Error, HttpResponse};
pub async fn login(
id: Identity,
credentials: BasicAuth,
config: web::Data<AppConfig>,
app_data: web::Data<AppData>,
) -> Result<HttpResponse, DomainError> {
let maybe_identity = id.identity();
let response = if let Some(identity) = maybe_identity {
@ -21,7 +21,7 @@ pub async fn login(
} else {
let credentials2 = credentials.clone();
let valid =
web::block(move || validate_basic_auth(credentials2, &config))
web::block(move || validate_basic_auth(credentials2, &app_data))
.await
.map_err(|_err| {
DomainError::new_thread_pool_error(_err.to_string())
@ -68,10 +68,10 @@ pub async fn index(id: Identity) -> String {
/// basic auth middleware function
pub fn validate_basic_auth(
credentials: BasicAuth,
config: &AppConfig,
app_data: &AppData,
) -> Result<bool, DomainError> {
let result = if let Some(password_ref) = credentials.password() {
let pool = &config.pool;
let pool = &app_data.pool;
let conn = pool.get()?;
let password = password_ref.clone().into_owned();
let valid = users::verify_password(

View File

@ -1,29 +1,26 @@
use actix_web::{get, post, web, HttpResponse};
use crate::errors::DomainError;
use crate::services::UserService;
use crate::utils::LogErrorResult;
use crate::AppConfig;
use crate::{actions, models};
use crate::{errors::DomainError, AppData};
use actix_web::error::ResponseError;
use validator::Validate;
/// Finds user by UID.
#[get("/get/users/{user_id}")]
pub async fn get_user(
config: web::Data<AppConfig>,
app_data: web::Data<AppData>,
user_id_param: web::Path<i32>,
) -> Result<HttpResponse, DomainError> {
let u_id = user_id_param.into_inner();
// use web::block to offload blocking Diesel code without blocking server thread
let res = web::block(move || {
let pool = &config.pool;
let pool = &app_data.pool;
let conn = pool.get()?;
actions::find_user_by_uid(u_id, &conn)
})
.await
.map_err(|err| DomainError::new_thread_pool_error(err.to_string()))
.log_err()?;
.map_err(|err| DomainError::new_thread_pool_error(err.to_string()))?;
if let Some(user) = res {
Ok(HttpResponse::Ok().json(user))
} else {
@ -55,17 +52,16 @@ pub async fn get_user2(
#[get("/get/users")]
pub async fn get_all_users(
config: web::Data<AppConfig>,
app_data: web::Data<AppData>,
) -> Result<HttpResponse, DomainError> {
// use web::block to offload blocking Diesel code without blocking server thread
let users = web::block(move || {
let pool = &config.pool;
let pool = &app_data.pool;
let conn = pool.get()?;
actions::get_all(&conn)
})
.await
.map_err(|err| DomainError::new_thread_pool_error(err.to_string()))
.log_err()?;
.map_err(|err| DomainError::new_thread_pool_error(err.to_string()))?;
debug!("{:?}", users);
@ -81,13 +77,13 @@ pub async fn get_all_users(
/// Inserts new user with name defined in form.
#[post("/do_registration")]
pub async fn add_user(
config: web::Data<AppConfig>,
app_data: web::Data<AppData>,
form: web::Json<models::NewUser>,
) -> Result<HttpResponse, impl ResponseError> {
// use web::block to offload blocking Diesel code without blocking server thread
let res = match form.0.validate() {
Ok(_) => web::block(move || {
let pool = &config.pool;
let pool = &app_data.pool;
let conn = pool.get()?;
actions::insert_new_user(form.0, &conn)
})

View File

@ -0,0 +1,89 @@
extern crate actix_demo;
use actix_demo::{AppConfig, AppData};
use actix_web::test;
use actix_web::App;
use diesel::SqliteConnection;
use diesel::r2d2::{self, ConnectionManager};
use env_logger::Env;
use std::io;
use std::io::ErrorKind;
use actix_demo::configure_app;
use actix_http::Request;
use actix_web::{dev as ax_dev, Error as AxError};
pub async fn test_app() -> impl ax_dev::Service<
Request = Request,
Response = ax_dev::ServiceResponse<impl ax_dev::MessageBody>,
Error = AxError,
> {
let _ = dotenv::dotenv()
.map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to set up env: {:?}", err),
)
})
.unwrap();
let _ = env_logger::builder()
.is_test(true)
.parse_env(Env::default().filter("ACTIX_DEMO_TEST_RUST_LOG"))
.try_init();
let connspec = ":memory:";
let manager = ConnectionManager::<SqliteConnection>::new(connspec);
let pool = r2d2::Pool::builder()
.build(manager)
.map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to create pool: {:?}", err),
)
})
.unwrap();
let _ = {
let conn = &pool
.get()
.map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Failed to get connection: {:?}", err),
)
})
.unwrap();
let migrations_dir = diesel_migrations::find_migrations_directory()
.map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Error finding migrations dir: {:?}", err),
)
})
.unwrap();
let _ = diesel_migrations::run_pending_migrations_in_directory(
conn,
&migrations_dir,
&mut io::sink(),
)
.map_err(|err| {
io::Error::new(
ErrorKind::Other,
format!("Error running migrations: {:?}", err),
)
})
.unwrap();
};
test::init_service(
App::new()
.configure(configure_app(AppData {
config: AppConfig { hash_cost: 8 },
pool,
}))
.wrap(actix_web::middleware::Logger::default()),
)
.await
}

47
tests/integration/main.rs Normal file
View File

@ -0,0 +1,47 @@
mod common;
#[cfg(test)]
mod tests {
use super::*;
extern crate actix_demo;
use actix_demo::models::ErrorModel;
use actix_web::dev::Service as _;
use actix_web::http::StatusCode;
use actix_web::test;
#[actix_rt::test]
async fn get_users_api_should_succeed() {
let req = test::TestRequest::get().uri("/api/get/users").to_request();
let resp = common::test_app().await.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::ACCEPTED);
let body: ErrorModel = test::read_body_json(resp).await;
assert_eq!(
body,
ErrorModel {
success: false,
reason: "Entity does not exist - No users available".to_owned()
}
);
log::debug!("{:?}", body);
}
#[actix_rt::test]
async fn get_user_api_should_succeed() {
let req = test::TestRequest::get()
.uri("/api/get/users/1")
.to_request();
let resp = common::test_app().await.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::ACCEPTED);
let body: ErrorModel = test::read_body_json(resp).await;
assert_eq!(
body,
ErrorModel {
success: false,
reason: "Entity does not exist - No user found with uid: 1"
.to_owned()
}
);
log::debug!("{:?}", body);
}
}