diff --git a/Cargo.lock b/Cargo.lock index 6ae13ce..baa4654 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1981,9 +1981,11 @@ version = "0.1.0" dependencies = [ "async-stream", "async-trait", + "chrono", "dotenv", "futures", "futures-util", + "once_cell", "portfolio-core", "portfolio-entity", "portfolio-migration", diff --git a/api/Cargo.toml b/api/Cargo.toml index ac54819..794181b 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -20,9 +20,15 @@ dotenv = "^0.15" serde_json = { version = "^1.0" } +chrono = "^0.4" + portfolio-entity = { path = "../entity" } portfolio-migration = { path = "../migration" } portfolio-core = { path = "../core" } [dependencies.sea-orm-rocket] version = "^0.5" + + +[dev-dependencies] +once_cell = "1.9.0" \ No newline at end of file diff --git a/api/src/lib.rs b/api/src/lib.rs index 1a793c7..bba53e5 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -12,6 +12,7 @@ mod guards; mod pool; mod requests; mod routes; +pub mod test; use pool::Db; @@ -29,8 +30,7 @@ async fn run_migrations(rocket: Rocket) -> fairing::Result { Ok(rocket) } -#[tokio::main] -async fn start() -> Result<(), rocket::Error> { +pub fn rocket() -> Rocket{ rocket::build() .attach(Db::init()) .attach(AdHoc::try_on_ignite("Migrations", run_migrations)) @@ -80,6 +80,11 @@ async fn start() -> Result<(), rocket::Error> { routes::admin::list_candidates, ]) .register("/", catchers![]) +} + +#[tokio::main] +async fn start() -> Result<(), rocket::Error> { + rocket() .launch() .await .map(|_| ()) diff --git a/api/src/pool.rs b/api/src/pool.rs index b031c6c..650d1c6 100644 --- a/api/src/pool.rs +++ b/api/src/pool.rs @@ -1,4 +1,4 @@ -use portfolio_core::sea_orm; +use portfolio_core::{sea_orm::{self}, util::get_memory_sqlite_connection}; use async_trait::async_trait; use sea_orm::ConnectOptions; @@ -22,20 +22,28 @@ impl sea_orm_rocket::Pool for SeaOrmPool { async fn init(_figment: &Figment) -> Result { dotenv::dotenv().ok(); + if std::env::var("TEST").is_ok() { + let conn = get_memory_sqlite_connection().await; + crate::test::run_test_migrations(&conn).await; + return Ok(Self { conn }); + } + + let database_url = std::env::var("DATABASE_URL").unwrap(); let mut options: ConnectOptions = database_url.into(); options .max_connections(1024) .min_connections(0) .connect_timeout(Duration::from_secs(3)); - - /* options + + /* options .max_connections(config.max_connections as u32) .min_connections(config.min_connections.unwrap_or_default()) .connect_timeout(Duration::from_secs(config.connect_timeout)); - if let Some(idle_timeout) = config.idle_timeout { - options.idle_timeout(Duration::from_secs(idle_timeout)); - } */ + if let Some(idle_timeout) = config.idle_timeout { + options.idle_timeout(Duration::from_secs(idle_timeout)); + } */ + let conn = sea_orm::Database::connect(options).await?; Ok(SeaOrmPool { conn }) @@ -44,4 +52,4 @@ impl sea_orm_rocket::Pool for SeaOrmPool { fn borrow(&self) -> &Self::Connection { &self.conn } -} +} \ No newline at end of file diff --git a/api/src/routes/admin.rs b/api/src/routes/admin.rs index e4eb796..9bc68ac 100644 --- a/api/src/routes/admin.rs +++ b/api/src/routes/admin.rs @@ -1,4 +1,4 @@ -use std::net::SocketAddr; +use std::net::{SocketAddr, IpAddr, Ipv4Addr}; use portfolio_core::{ crypto::random_8_char_string, @@ -17,9 +17,10 @@ use crate::{guards::request::auth::AdminAuth, pool::Db, requests}; pub async fn login( conn: Connection<'_, Db>, login_form: Json, - ip_addr: SocketAddr, + // ip_addr: SocketAddr, // TODO uncomment in production cookies: &CookieJar<'_>, ) -> Result> { + let ip_addr: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); let db = conn.into_inner(); let session_token_key = AdminService::login( db, diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index e180352..2175b5d 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -1,4 +1,4 @@ -use std::net::SocketAddr; +use std::net::{SocketAddr, IpAddr, Ipv4Addr}; use portfolio_core::candidate_details::ApplicationDetails; use portfolio_core::services::application_service::ApplicationService; @@ -19,9 +19,10 @@ use crate::{guards::request::auth::CandidateAuth, pool::Db, requests}; pub async fn login( conn: Connection<'_, Db>, login_form: Json, - ip_addr: SocketAddr, + // ip_addr: SocketAddr, // TODO uncomment in production cookies: &CookieJar<'_>, ) -> Result> { + let ip_addr: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0); let db = conn.into_inner(); let session_token_key = CandidateService::login( db, diff --git a/api/src/test.rs b/api/src/test.rs new file mode 100644 index 0000000..2ce5bde --- /dev/null +++ b/api/src/test.rs @@ -0,0 +1,40 @@ +use entity::admin; +use portfolio_core::{sea_orm::{DbConn, Set, ActiveModelTrait}, crypto, services::application_service::ApplicationService}; + +pub const ADMIN_ID: i32 = 1; +pub const ADMIN_PASSWORD: &'static str = "test"; + +pub const APPLICATION_ID: i32 = 103151; +pub const CANDIDATE_PASSWORD: &'static str = "test"; +pub const PERSONAL_ID_NUMBER: &'static str = "0101010000"; + +pub async fn run_test_migrations(db: &DbConn) { + let (pubkey, priv_key) = crypto::create_identity(); + let priv_key = crypto::encrypt_password( + priv_key, + ADMIN_PASSWORD.to_string() + ).await.unwrap(); + let password_hash = crypto::hash_password(ADMIN_PASSWORD.to_string()).await.unwrap(); + + admin::ActiveModel { + id: Set(ADMIN_ID), + name: Set("admin pepa".to_string()), + public_key: Set(pubkey), + private_key: Set(priv_key), + password: Set(password_hash), + created_at: Set(chrono::Utc::now().naive_utc()), + updated_at: Set(chrono::Utc::now().naive_utc()), + } + .insert(db) + .await + .unwrap(); + + ApplicationService::create_candidate_with_parent( + db, + APPLICATION_ID, + &CANDIDATE_PASSWORD.to_string(), + PERSONAL_ID_NUMBER.to_string() + ) + .await + .unwrap(); +} \ No newline at end of file diff --git a/api/tests/candidate.rs b/api/tests/candidate.rs new file mode 100644 index 0000000..4d47a0c --- /dev/null +++ b/api/tests/candidate.rs @@ -0,0 +1,33 @@ +mod common; +use common::*; +use portfolio_api::test::APPLICATION_ID; +use rocket::http::Status; + +#[test] +fn test_login_valid_credentials() { + let client = test_client().lock().unwrap(); + let _response = candidate_login(&client); +} + +#[test] +fn test_create_candidate() { + let client = test_client().lock().unwrap(); + let cookies = admin_login(&client); + let password = create_candidate(&client, cookies, 1031511, "0".to_string()); + + assert_eq!(password.len(), 8); +} + +#[test] +fn test_auth_candidate() { + let client = test_client().lock().unwrap(); + let cookies = candidate_login(&client); + let response = client + .get("/candidate/whoami") + .cookie(cookies.0) + .cookie(cookies.1) + .dispatch(); + + assert_eq!(response.status(), Status::Ok); + assert_eq!(response.into_string().unwrap(), APPLICATION_ID.to_string()); +} \ No newline at end of file diff --git a/api/tests/common.rs b/api/tests/common.rs new file mode 100644 index 0000000..1214b48 --- /dev/null +++ b/api/tests/common.rs @@ -0,0 +1,62 @@ +use once_cell::sync::OnceCell; +use std::sync::Mutex; +use rocket::http::{Cookie, Status}; +use rocket::local::blocking::{Client}; +use portfolio_api::rocket; + +use portfolio_api::test::{ADMIN_ID, ADMIN_PASSWORD, APPLICATION_ID, CANDIDATE_PASSWORD}; + +pub fn test_client() -> &'static Mutex { + static INSTANCE: OnceCell> = OnceCell::new(); + INSTANCE.get_or_init(|| { + let rocket = rocket(); + Mutex::from(Client::tracked(rocket).expect("valid rocket instance")) + }) +} + +pub fn candidate_login(client: &Client) -> (Cookie, Cookie) { + let response = client + .post("/candidate/login") + .body(format!("{{ + \"application_id\": {}, + \"password\": \"{}\" + }}", APPLICATION_ID, CANDIDATE_PASSWORD)) + .dispatch(); + + ( + response.cookies().get("id").unwrap().to_owned(), + response.cookies().get("key").unwrap().to_owned() + ) +} + +pub fn admin_login(client: &Client) -> (Cookie, Cookie) { + let response = client + .post("/admin/login") + .body(format!("{{ + \"admin_id\": {}, + \"password\": \"{}\" + }}", ADMIN_ID, ADMIN_PASSWORD)) + .dispatch(); + + println!("{:?}", response); + ( + response.cookies().get("id").unwrap().to_owned(), + response.cookies().get("key").unwrap().to_owned(), + ) +} + +pub fn create_candidate(client: &Client, cookies: (Cookie, Cookie), id: i32, pid: String) -> String { + let response = client + .post("/admin/create") + .body(format!("{{ + \"application_id\": {}, + \"personal_id_number\": \"{}\" + }}", id, pid)) + .cookie(cookies.0) + .cookie(cookies.1) + .dispatch(); + + assert_eq!(response.status(), Status::Ok); + + response.into_string().unwrap() +} \ No newline at end of file diff --git a/core/src/util.rs b/core/src/util.rs index ab47922..a988644 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -1,30 +1,22 @@ -#[cfg(test)] +use entity::{admin, candidate, parent, session}; +use sea_orm::{Schema, Database, DbConn}; +use sea_orm::{sea_query::TableCreateStatement, ConnectionTrait, DbBackend}; + + pub async fn get_memory_sqlite_connection() -> sea_orm::DbConn { - use entity::{admin, candidate, parent, session}; - use sea_orm::{Schema, Database, DbConn}; - use sea_orm::{sea_query::TableCreateStatement, ConnectionTrait, DbBackend}; - let base_url = "sqlite::memory:"; - let db: DbConn = Database::connect(base_url).await.unwrap(); + let db: DbConn = Database::connect(base_url).await.unwrap(); - let schema = Schema::new(DbBackend::Sqlite); - let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity); - let stmt2: TableCreateStatement = schema.create_table_from_entity(admin::Entity); - let stmt3: TableCreateStatement = schema.create_table_from_entity(session::Entity); - let stmt4: TableCreateStatement = schema.create_table_from_entity(parent::Entity); - db.execute(db.get_database_backend().build(&stmt)) - .await - .unwrap(); - db.execute(db.get_database_backend().build(&stmt2)) - .await - .unwrap(); - db.execute(db.get_database_backend().build(&stmt3)) - .await - .unwrap(); - db.execute(db.get_database_backend().build(&stmt4)) - .await - .unwrap(); - db + let schema = Schema::new(DbBackend::Sqlite); + let stmt: TableCreateStatement = schema.create_table_from_entity(candidate::Entity); + let stmt2: TableCreateStatement = schema.create_table_from_entity(admin::Entity); + let stmt3: TableCreateStatement = schema.create_table_from_entity(session::Entity); + let stmt4: TableCreateStatement = schema.create_table_from_entity(parent::Entity); + db.execute(db.get_database_backend().build(&stmt)).await.unwrap(); + db.execute(db.get_database_backend().build(&stmt2)).await.unwrap(); + db.execute(db.get_database_backend().build(&stmt3)).await.unwrap(); + db.execute(db.get_database_backend().build(&stmt4)).await.unwrap(); + db } #[cfg(test)]