diff --git a/Cargo.lock b/Cargo.lock index 91afc04..6ae13ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2025,6 +2025,7 @@ dependencies = [ "sea-orm", "secrecy", "serde", + "serial_test", "thiserror", "tokio", ] @@ -2708,6 +2709,32 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92761393ee4dc3ff8f4af487bd58f4307c9329bbedea02cac0089ad9c411e153" +dependencies = [ + "dashmap", + "futures", + "lazy_static", + "log", + "parking_lot 0.12.1", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b6f5d1c3087fb119617cff2966fe3808a80e5eb59a8c1601d5994d66f4346a5" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sha1" version = "0.10.5" diff --git a/api/src/lib.rs b/api/src/lib.rs index b6517b1..22a905d 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -41,11 +41,24 @@ async fn start() -> Result<(), rocket::Error> { routes![ routes::candidate::login, routes::candidate::whoami, - routes::candidate::fill_details, routes::candidate::get_details, - routes::candidate::upload_cover_letter, + ], + ) + .mount( + "/candidate/add", + routes![ + routes::candidate::add_details, routes::candidate::upload_portfolio_letter, routes::candidate::upload_portfolio_zip, + routes::candidate::upload_cover_letter, + ], + ) + .mount( + "/candidate/portfolio", + routes![ + routes::candidate::submit_portfolio, + routes::candidate::is_portfolio_prepared, + routes::candidate::is_portfolio_submitted, ], ) .mount( diff --git a/api/src/routes/candidate.rs b/api/src/routes/candidate.rs index c66d4db..3d2dcd2 100644 --- a/api/src/routes/candidate.rs +++ b/api/src/routes/candidate.rs @@ -2,7 +2,7 @@ use std::net::SocketAddr; use portfolio_core::candidate_details::ApplicationDetails; use portfolio_core::services::application_service::ApplicationService; -use portfolio_core::services::candidate_service::{CandidateService}; +use portfolio_core::services::candidate_service::CandidateService; use requests::LoginRequest; use rocket::http::{Cookie, CookieJar, Status}; use rocket::response::status::Custom; @@ -59,7 +59,7 @@ pub async fn whoami(session: CandidateAuth) -> Result> { } #[post("/details", data = "
")] -pub async fn fill_details( +pub async fn add_details( conn: Connection<'_, Db>, details: Json, session: CandidateAuth, @@ -68,7 +68,8 @@ pub async fn fill_details( let form = details.into_inner(); let candidate: entity::candidate::Model = session.into(); // TODO: don't return candidate from session - let candidate_parent = ApplicationService::add_all_details(db, candidate.application, form).await; + let candidate_parent = + ApplicationService::add_all_details(db, candidate.application, form).await; if candidate_parent.is_err() { // TODO cleanup @@ -95,18 +96,24 @@ pub async fn get_details( // let handle = tokio::spawn(async move { let details = ApplicationService::decrypt_all_details(db, candidate.application, password) .await - .map_err(|e| Custom(Status::from_code(e.code()).unwrap_or_default(), e.to_string())); + .map_err(|e| { + Custom( + Status::from_code(e.code()).unwrap_or_default(), + e.to_string(), + ) + }); details.map(|d| Json(d)) } -#[post("/coverletter", data = "")] +#[post("/cover_letter", data = "")] pub async fn upload_cover_letter( session: CandidateAuth, letter: Letter, ) -> Result> { let candidate: entity::candidate::Model = session.into(); - let candidate = CandidateService::add_cover_letter(candidate.application, letter.into()).await; + let candidate = + CandidateService::add_cover_letter_to_cache(candidate.application, letter.into()).await; if candidate.is_err() { // TODO cleanup @@ -120,7 +127,17 @@ pub async fn upload_cover_letter( Ok("Letter added".to_string()) } -#[post("/portfolioletter", data = "")] +// TODO: JSON +#[get["/is_cover_letter"]] +pub async fn is_cover_letter(session: CandidateAuth) -> Result> { + let candidate: entity::candidate::Model = session.into(); + + let exists = CandidateService::is_cover_letter(candidate.application).await; + + Ok(exists.to_string()) +} + +#[post("/portfolio_letter", data = "")] pub async fn upload_portfolio_letter( session: CandidateAuth, letter: Letter, @@ -128,7 +145,7 @@ pub async fn upload_portfolio_letter( let candidate: entity::candidate::Model = session.into(); let candidate = - CandidateService::add_portfolio_letter(candidate.application, letter.into()).await; + CandidateService::add_portfolio_letter_to_cache(candidate.application, letter.into()).await; if candidate.is_err() { // TODO cleanup @@ -142,7 +159,17 @@ pub async fn upload_portfolio_letter( Ok("Letter added".to_string()) } -#[post("/portfolio", data = "")] +// TODO: JSON +#[get["/is_portfolio_letter"]] +pub async fn is_portfolio_letter(session: CandidateAuth) -> Result> { + let candidate: entity::candidate::Model = session.into(); + + let exists = CandidateService::is_portfolio_letter(candidate.application).await; + + Ok(exists.to_string()) +} + +#[post("/portfolio_zip", data = "")] pub async fn upload_portfolio_zip( session: CandidateAuth, portfolio: Portfolio, @@ -150,7 +177,7 @@ pub async fn upload_portfolio_zip( let candidate: entity::candidate::Model = session.into(); let candidate = - CandidateService::add_portfolio_zip(candidate.application, portfolio.into()).await; + CandidateService::add_portfolio_zip_to_cache(candidate.application, portfolio.into()).await; if candidate.is_err() { // TODO cleanup @@ -161,5 +188,79 @@ pub async fn upload_portfolio_zip( )); } - Ok("Letter added".to_string()) + Ok("Portfolio added".to_string()) +} + +// TODO: JSON +#[get["/is_portfolio_zip"]] +pub async fn is_portfolio_zip(session: CandidateAuth) -> Result> { + let candidate: entity::candidate::Model = session.into(); + + let exists = CandidateService::is_portfolio_zip(candidate.application).await; + + Ok(exists.to_string()) +} + +#[post("/submit")] +pub async fn submit_portfolio( + conn: Connection<'_, Db>, + session: CandidateAuth, +) -> Result> { + let db = conn.into_inner(); + + let candidate: entity::candidate::Model = session.into(); + + let submit = CandidateService::add_portfolio(candidate.application, &db).await; + + if submit.is_err() { + let e = submit.err().unwrap(); + // Delete on critical error + // TODO: VĂ­ce kontrol? + if e.code() == 500 { + // Cleanup + CandidateService::delete_portfolio(candidate.application) + .await + .unwrap(); + } + return Err(Custom( + Status::from_code(e.code()).unwrap_or_default(), + e.to_string(), + )); + } + + Ok("Portfolio submitted".to_string()) +} + +#[get("/is_prepared")] +pub async fn is_portfolio_prepared(session: CandidateAuth) -> Result> { + let candidate: entity::candidate::Model = session.into(); + + let is_ok = CandidateService::is_portfolio_prepared(candidate.application).await; + + if !is_ok { + // TODO: Correct error + return Err(Custom( + Status::from_code(404).unwrap_or_default(), + "Portfolio not prepared".to_string(), + )); + } + + Ok("Portfolio ok".to_string()) +} + +#[get("/is_submitted")] +pub async fn is_portfolio_submitted(session: CandidateAuth) -> Result> { + let candidate: entity::candidate::Model = session.into(); + + let is_ok = CandidateService::is_portfolio_submitted(candidate.application).await; + + if !is_ok { + // TODO: Correct error + return Err(Custom( + Status::from_code(404).unwrap_or_default(), + "Portfolio not submitted".to_string(), + )); + } + + Ok("Portfolio ok".to_string()) } diff --git a/core/Cargo.toml b/core/Cargo.toml index bf6c2ac..b2f28bc 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -47,4 +47,5 @@ features = [ [dev-dependencies] tokio = { version = "^1.21", features = ["macros"] } -async-tempfile = "^0.2" \ No newline at end of file +async-tempfile = "^0.2" +serial_test = "0.9.0" \ No newline at end of file diff --git a/core/src/crypto.rs b/core/src/crypto.rs index 9eaf538..c8344f1 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -238,8 +238,8 @@ async fn age_decrypt_with_private_key( }; let mut decrypt_writer = decryptor.decrypt_async(iter::once( - &age::x25519::Identity::from_str(key).map_err(|e| ServiceError::AgeKeyError(e.to_string()))? - as &dyn age::Identity, + &age::x25519::Identity::from_str(key) + .map_err(|e| ServiceError::AgeKeyError(e.to_string()))? as &dyn age::Identity, ))?; decrypt_writer.read_to_end(output_buffer).await?; @@ -289,12 +289,18 @@ pub async fn encrypt_file_with_recipients>( tokio::io::AsyncReadExt::read_to_end(&mut plain_file, &mut plain_file_contents).await?; + drop(plain_file); + age_encrypt_with_recipients( plain_file_contents.as_slice(), &mut cipher_file, &recipients, ) - .await + .await?; + + tokio::io::AsyncWriteExt::shutdown(&mut cipher_file).await?; + + Ok(()) } pub async fn decrypt_file_with_private_key>( diff --git a/core/src/error.rs b/core/src/error.rs index 5aebb06..4c8f4f1 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -89,7 +89,7 @@ impl ServiceError { ServiceError::AesError(_) => 500, ServiceError::ArgonConfigError(_) => 500, //TODO: Correct code - ServiceError::IncompletePortfolio => 500, + ServiceError::IncompletePortfolio => 406, ServiceError::ZipError(_) => 500, } } diff --git a/core/src/services/candidate_service.rs b/core/src/services/candidate_service.rs index 592ce8d..f57dba9 100644 --- a/core/src/services/candidate_service.rs +++ b/core/src/services/candidate_service.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use entity::candidate; use sea_orm::{prelude::Uuid, DbConn}; @@ -18,6 +18,12 @@ const FIELD_OF_STUDY_PREFIXES: [&str; 3] = ["101", "102", "103"]; pub struct CandidateService; impl CandidateService { + // Get root path or local directory + fn get_file_store_path() -> PathBuf { + dotenv::dotenv().ok(); + Path::new(&std::env::var("STORE_PATH").unwrap_or_else(|_| "".to_string())).to_path_buf() + } + /// Creates a new candidate with: /// Encrypted personal identification number /// Hashed password @@ -47,13 +53,12 @@ impl CandidateService { let (pubkey, priv_key_plain_text) = crypto::create_identity(); - let encrypted_priv_key = crypto::encrypt_password(priv_key_plain_text, plain_text_password.to_string()).await?; + let encrypted_priv_key = + crypto::encrypt_password(priv_key_plain_text, plain_text_password.to_string()).await?; let hashed_personal_id_number = hash_password(personal_id_number).await?; - // TODO: Specify root path in config? - tokio::fs::create_dir_all(Path::new(&application_id.to_string()).join("cache")).await?; - + tokio::fs::create_dir_all(Self::get_file_store_path().join(&application_id.to_string()).join("cache")).await?; let candidate = Mutation::create_candidate( db, @@ -76,7 +81,7 @@ impl CandidateService { Ok(model) } - pub fn is_set_up(candidate: &candidate::Model) -> bool { + pub fn is_candidate_info(candidate: &candidate::Model) -> bool { candidate.name.is_some() && candidate.surname.is_some() && candidate.birthplace.is_some() @@ -94,7 +99,7 @@ impl CandidateService { data: Vec, filename: &str, ) -> Result<(), ServiceError> { - let cache_path = Path::new(&candidate_id.to_string()).join("cache"); + let cache_path = Self::get_file_store_path().join(&candidate_id.to_string()).join("cache"); let mut file = tokio::fs::File::create(cache_path.join(filename)).await?; @@ -103,79 +108,117 @@ impl CandidateService { Ok(()) } - pub async fn add_cover_letter(candidate_id: i32, letter: Vec) -> Result<(), ServiceError> { + pub async fn add_cover_letter_to_cache( + candidate_id: i32, + letter: Vec, + ) -> Result<(), ServiceError> { Self::write_portfolio_file(candidate_id, letter, "MOTIVACNI_DOPIS.pdf").await } - pub async fn add_portfolio_letter( + pub async fn is_cover_letter(candidate_id: i32) -> bool { + let cache_path = Self::get_file_store_path().join(&candidate_id.to_string()).join("cache"); + + tokio::fs::metadata(cache_path.join(cache_path.join("MOTIVACNI_DOPIS.pdf"))) + .await + .is_ok() + } + + pub async fn add_portfolio_letter_to_cache( candidate_id: i32, letter: Vec, ) -> Result<(), ServiceError> { Self::write_portfolio_file(candidate_id, letter, "PORTFOLIO.pdf").await } - pub async fn add_portfolio_zip(candidate_id: i32, zip: Vec) -> Result<(), ServiceError> { + pub async fn is_portfolio_letter(candidate_id: i32) -> bool { + let cache_path = Self::get_file_store_path().join(&candidate_id.to_string()).join("cache"); + + tokio::fs::metadata(cache_path.join(cache_path.join("PORTFOLIO.pdf"))) + .await + .is_ok() + } + + pub async fn add_portfolio_zip_to_cache( + candidate_id: i32, + zip: Vec, + ) -> Result<(), ServiceError> { Self::write_portfolio_file(candidate_id, zip, "PORTFOLIO.zip").await } - pub async fn is_portfolio_complete(candidate_id: i32) -> bool { - let cache_path = Path::new(&candidate_id.to_string()).join("cache"); + pub async fn is_portfolio_zip(candidate_id: i32) -> bool { + let cache_path = Self::get_file_store_path().join(&candidate_id.to_string()).join("cache"); - tokio::fs::metadata(cache_path.join("MOTIVACNI_DOPIS.pdf")) + tokio::fs::metadata(cache_path.join(cache_path.join("PORTFOLIO.zip"))) .await .is_ok() - && tokio::fs::metadata(cache_path.join("PORTFOLIO.pdf")) - .await - .is_ok() - && tokio::fs::metadata(cache_path.join("PORTFOLIO.zip")) - .await - .is_ok() } - pub async fn submit_portfolio(candidate_id: i32, db: &DbConn) -> Result<(), ServiceError> { - let path = Path::new(&candidate_id.to_string()).to_path_buf(); + pub async fn is_portfolio_prepared(candidate_id: i32) -> bool { + let cache_path = Self::get_file_store_path().join(&candidate_id.to_string()).join("cache"); + + let filenames = vec!["MOTIVACNI_DOPIS.pdf", "PORTFOLIO.pdf", "PORTFOLIO.zip"]; + + for filename in filenames { + if !tokio::fs::metadata(cache_path.join(filename)).await.is_ok() { + return false; + } + } + + true + } + + pub async fn delete_cache(candidate_id: i32) -> Result<(), ServiceError> { + let cache_path = Self::get_file_store_path().join(&candidate_id.to_string()).join("cache"); + + tokio::fs::remove_dir_all(&cache_path).await?; + // Recreate blank cache directory + tokio::fs::create_dir_all(&cache_path).await?; + + Ok(()) + } + + pub async fn add_portfolio(candidate_id: i32, db: &DbConn) -> Result<(), ServiceError> { + let path = Self::get_file_store_path().join(&candidate_id.to_string()).to_path_buf(); let cache_path = path.join("cache"); - if Self::is_portfolio_complete(candidate_id).await == false { + if Self::is_portfolio_prepared(candidate_id).await == false { return Err(ServiceError::IncompletePortfolio); } let mut archive = tokio::fs::File::create(path.join("PORTFOLIO.zip")).await?; - let mut writer = async_zip::write::ZipFileWriter::new(&mut archive); - for entry in vec!["MOTIVACNI_DOPIS.pdf", "PORTFOLIO.pdf", "PORTFOLIO.zip"] { - let mut entry_file = tokio::fs::File::open(cache_path.join(entry)) - .await?; + let mut buffer = vec![vec![], vec![], vec![]]; - let mut contents = vec![]; + let filenames = vec!["MOTIVACNI_DOPIS.pdf", "PORTFOLIO.pdf", "PORTFOLIO.zip"]; - entry_file - .read_to_end(&mut contents) - .await?; + for (index, entry) in buffer.iter_mut().enumerate() { + let filename = filenames[index]; + let mut entry_file = tokio::fs::File::open(cache_path.join(filename)).await?; - let builder = - async_zip::ZipEntryBuilder::new(entry.to_string(), async_zip::Compression::Deflate); - - let mut entry_writer = writer - .write_entry_stream(builder) - .await?; - - // TODO: write_all_buf? - entry_writer - .write_all(&mut contents) - .await?; + entry_file.read_to_end(entry).await?; } - // TODO: Ne unwrap - writer.close().await.unwrap(); - archive.shutdown().await.unwrap(); + Self::delete_cache(candidate_id).await?; - let admin_public_keys = Query::get_all_admin_public_keys(db) - .await?; + for (index, entry) in buffer.iter_mut().enumerate() { + let filename = filenames[index]; + let builder = async_zip::ZipEntryBuilder::new( + filename.to_string(), + async_zip::Compression::Deflate, + ); - let candidate = Query::find_candidate_by_id(db, candidate_id).await? + writer.write_entry_whole(builder, &entry).await?; + } + + writer.close().await?; + archive.shutdown().await?; + + let admin_public_keys = Query::get_all_admin_public_keys(db).await?; + + let candidate = Query::find_candidate_by_id(db, candidate_id) + .await? .ok_or(ServiceError::CandidateNotFound)?; let candidate_public_key = candidate.public_key; @@ -187,27 +230,58 @@ impl CandidateService { recipients.append(&mut admin_public_keys_refrence); + let final_path = path.join("PORTFOLIO.zip"); + let Ok(_) = crypto::encrypt_file_with_recipients( - path.join("PORTFOLIO.zip"), - path.join("PORTFOLIO.zip"), + &final_path, + &final_path.with_extension("age"), recipients, ) .await else { return Err(ServiceError::CryptoEncryptFailed); }; + tokio::fs::remove_file(final_path).await?; + Ok(()) } + pub async fn delete_portfolio(candidate_id: i32) -> Result<(), ServiceError> { + let path = Self::get_file_store_path().join(&candidate_id.to_string()).to_path_buf(); + + let portfolio_path = path.join("PORTFOLIO.zip"); + let portfolio_age_path = portfolio_path.with_extension("age"); + + if tokio::fs::metadata(&portfolio_path).await.is_ok() { + tokio::fs::remove_file(&portfolio_path).await?; + } + + if tokio::fs::metadata(&portfolio_age_path).await.is_ok() { + tokio::fs::remove_file(&portfolio_age_path).await?; + } + + Ok(()) + } + + pub async fn is_portfolio_submitted(candidate_id: i32) -> bool { + let path = Self::get_file_store_path().join(&candidate_id.to_string()).to_path_buf(); + + tokio::fs::metadata(path.join("PORTFOLIO.age")).await.is_ok() + } + pub async fn get_portfolio(candidate_id: i32, db: &DbConn) -> Result, ServiceError> { - let candidate = Query::find_candidate_by_id(db, candidate_id).await? + let path = Self::get_file_store_path().join(&candidate_id.to_string()).to_path_buf(); + + let candidate = Query::find_candidate_by_id(db, candidate_id) + .await? .ok_or(ServiceError::CandidateNotFound)?; let candidate_public_key = candidate.public_key; - let path = Path::new(&candidate_id.to_string()).join("PORTFOLIO.zip"); + let path = path.join("PORTFOLIO.age"); - let buffer = crypto::decrypt_file_with_private_key_as_buffer(path, &candidate_public_key).await?; + let buffer = + crypto::decrypt_file_with_private_key_as_buffer(path, &candidate_public_key).await?; Ok(buffer) } @@ -269,6 +343,7 @@ impl CandidateService { #[cfg(test)] mod tests { use sea_orm::{Database, DbConn}; + use serial_test::serial; use crate::util::get_memory_sqlite_connection; use crate::{crypto, services::candidate_service::CandidateService, Mutation}; @@ -280,6 +355,10 @@ mod tests { use crate::candidate_details::ApplicationDetails; use crate::services::application_service::ApplicationService; + use std::path::{PathBuf}; + + const APPLICATION_ID: i32 = 103151; + #[tokio::test] async fn test_application_id_validation() { assert!(CandidateService::is_application_id_valid(101_101)); @@ -299,12 +378,12 @@ mod tests { let secret_message = "trnka".to_string(); - let candidate = CandidateService::create(&db, 103151, &plain_text_password, "".to_string()) + let candidate = CandidateService::create(&db, APPLICATION_ID, &plain_text_password, "".to_string()) .await .ok() .unwrap(); - Mutation::create_parent(&db, 103151).await.unwrap(); + Mutation::create_parent(&db, APPLICATION_ID).await.unwrap(); let encrypted_message = crypto::encrypt_password_with_recipients(&secret_message, &vec![&candidate.public_key]) @@ -327,9 +406,9 @@ mod tests { #[cfg(test)] async fn put_user_data(db: &DbConn) -> (candidate::Model, parent::Model) { let plain_text_password = "test".to_string(); - let (candidate, parent) = ApplicationService::create_candidate_with_parent( + let (candidate, _parent) = ApplicationService::create_candidate_with_parent( &db, - 103151, + APPLICATION_ID, &plain_text_password, "".to_string(), ) @@ -384,4 +463,246 @@ mod tests { assert_eq!(dec_details.name, "test"); // TODO: test every element assert_eq!(dec_details.parent_surname, "test"); } + + #[cfg(test)] + async fn create_data_store_temp_dir(application_id: i32) -> (PathBuf, PathBuf, PathBuf) { + let random_number: u32 = rand::Rng::gen(&mut rand::thread_rng()); + + let temp_dir = std::env::temp_dir().join("portfolio_test_tempdir").join(random_number.to_string()); + let application_dir = temp_dir.join(application_id.to_string()); + let application_cache_dir = application_dir.join("cache"); + + tokio::fs::create_dir_all(application_cache_dir.clone()).await.unwrap(); + + std::env::set_var("STORE_PATH", temp_dir.to_str().unwrap()); + + (temp_dir, application_dir, application_cache_dir) + } + + #[cfg(test)] + async fn clear_data_store_temp_dir(temp_dir: PathBuf) { + tokio::fs::remove_dir_all(temp_dir).await.unwrap(); + } + + #[tokio::test] + #[serial] + async fn test_folder_creation() { + let db = get_memory_sqlite_connection().await; + let plain_text_password = "test".to_string(); + + let temp_dir = std::env::temp_dir().join("portfolio_test_tempdir").join("create_folder"); + std::env::set_var("STORE_PATH", temp_dir.to_str().unwrap()); + + CandidateService::create(&db, APPLICATION_ID, &plain_text_password, "".to_string()) + .await + .ok() + .unwrap(); + + assert!(tokio::fs::metadata(temp_dir.join(APPLICATION_ID.to_string())).await.is_ok()); + assert!(tokio::fs::metadata(temp_dir.join(APPLICATION_ID.to_string()).join("cache")).await.is_ok()); + + tokio::fs::remove_dir_all(temp_dir).await.unwrap(); + } + + #[tokio::test] + #[serial] + async fn test_write_portfolio_file() { + let (temp_dir, _, application_cache_dir) = create_data_store_temp_dir(APPLICATION_ID).await; + + CandidateService::write_portfolio_file(APPLICATION_ID, vec![0], "test").await.unwrap(); + + assert!(tokio::fs::metadata(application_cache_dir.join("test")).await.is_ok()); + + clear_data_store_temp_dir(temp_dir).await; + } + + #[tokio::test] + #[serial] + async fn test_add_cover_letter_to_cache() { + let (temp_dir, _, application_cache_dir) = create_data_store_temp_dir(APPLICATION_ID).await; + + CandidateService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + assert!(tokio::fs::metadata(application_cache_dir.join("MOTIVACNI_DOPIS.pdf")).await.is_ok()); + + clear_data_store_temp_dir(temp_dir).await; + } + + #[tokio::test] + #[serial] + async fn test_is_cover_letter() { + let (temp_dir, _, _) = create_data_store_temp_dir(APPLICATION_ID).await; + + CandidateService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + assert!(CandidateService::is_cover_letter(APPLICATION_ID).await); + + clear_data_store_temp_dir(temp_dir).await; + } + + #[tokio::test] + #[serial] + async fn test_add_portfolio_letter_to_cache() { + let (temp_dir, _, application_cache_dir) = create_data_store_temp_dir(APPLICATION_ID).await; + + CandidateService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + assert!(tokio::fs::metadata(application_cache_dir.join("PORTFOLIO.pdf")).await.is_ok()); + + clear_data_store_temp_dir(temp_dir).await; + } + + #[tokio::test] + #[serial] + async fn test_is_portfolio_letter() { + let (temp_dir, _, _) = create_data_store_temp_dir(APPLICATION_ID).await; + + CandidateService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + assert!(CandidateService::is_portfolio_letter(APPLICATION_ID).await); + + clear_data_store_temp_dir(temp_dir).await; + } + + #[tokio::test] + #[serial] + async fn test_add_portfolio_zip_to_cache() { + let (temp_dir, _, application_cache_dir) = create_data_store_temp_dir(APPLICATION_ID).await; + + CandidateService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + assert!(tokio::fs::metadata(application_cache_dir.join("PORTFOLIO.zip")).await.is_ok()); + + clear_data_store_temp_dir(temp_dir).await; + } + + #[tokio::test] + #[serial] + async fn test_is_portfolio_zip() { + let (temp_dir, _, _) = create_data_store_temp_dir(APPLICATION_ID).await; + + CandidateService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + assert!(CandidateService::is_portfolio_zip(APPLICATION_ID).await); + + clear_data_store_temp_dir(temp_dir).await; + } + + #[tokio::test] + #[serial] + async fn test_is_portfolio_prepared() { + let (temp_dir, _, _) = create_data_store_temp_dir(APPLICATION_ID).await; + + CandidateService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + CandidateService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + CandidateService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + assert!(CandidateService::is_portfolio_prepared(APPLICATION_ID).await); + + clear_data_store_temp_dir(temp_dir).await; + + let (temp_dir, _, _) = create_data_store_temp_dir(APPLICATION_ID).await; + + CandidateService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + //CandidateService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + CandidateService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + assert!(!CandidateService::is_portfolio_prepared(APPLICATION_ID).await); + + clear_data_store_temp_dir(temp_dir).await; + } + + #[tokio::test] + #[serial] + async fn test_delete_cache() { + let (temp_dir, _, _) = create_data_store_temp_dir(APPLICATION_ID).await; + + CandidateService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + assert!(CandidateService::is_portfolio_zip(APPLICATION_ID).await); + + CandidateService::delete_cache(APPLICATION_ID).await.unwrap(); + + assert!(!CandidateService::is_portfolio_zip(APPLICATION_ID).await); + + clear_data_store_temp_dir(temp_dir).await; + } + + #[tokio::test] + #[serial] + async fn test_add_portfolio() { + let (temp_dir, application_dir, _) = create_data_store_temp_dir(APPLICATION_ID).await; + + let db = get_memory_sqlite_connection().await; + put_user_data(&db).await; + + CandidateService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + CandidateService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + CandidateService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + CandidateService::add_portfolio(APPLICATION_ID, &db).await.unwrap(); + + assert!(tokio::fs::metadata(application_dir.join("PORTFOLIO.age")).await.is_ok()); + + clear_data_store_temp_dir(temp_dir).await; + } + + #[tokio::test] + #[serial] + async fn test_delete_portfolio() { + let (temp_dir, application_dir, _) = create_data_store_temp_dir(APPLICATION_ID).await; + + let db = get_memory_sqlite_connection().await; + put_user_data(&db).await; + + CandidateService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + CandidateService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + CandidateService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + CandidateService::add_portfolio(APPLICATION_ID, &db).await.unwrap(); + + assert!(tokio::fs::metadata(application_dir.join("PORTFOLIO.age")).await.is_ok()); + + CandidateService::delete_portfolio(APPLICATION_ID).await.unwrap(); + + assert!(!tokio::fs::metadata(application_dir.join("PORTFOLIO.age")).await.is_ok()); + + clear_data_store_temp_dir(temp_dir).await; + } + + #[tokio::test] + #[serial] + async fn test_is_portfolio_submitted() { + let (temp_dir, _, _) = create_data_store_temp_dir(APPLICATION_ID).await; + + let db = get_memory_sqlite_connection().await; + put_user_data(&db).await; + + CandidateService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + CandidateService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + CandidateService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + CandidateService::add_portfolio(APPLICATION_ID, &db).await.unwrap(); + + assert!(CandidateService::is_portfolio_submitted(APPLICATION_ID).await); + + clear_data_store_temp_dir(temp_dir).await; + + let (temp_dir, application_dir, _) = create_data_store_temp_dir(APPLICATION_ID).await; + + CandidateService::add_cover_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + CandidateService::add_portfolio_letter_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + CandidateService::add_portfolio_zip_to_cache(APPLICATION_ID, vec![0]).await.unwrap(); + + CandidateService::add_portfolio(APPLICATION_ID, &db).await.unwrap(); + + tokio::fs::remove_file(application_dir.join("PORTFOLIO.age")).await.unwrap(); + + assert!(!CandidateService::is_portfolio_submitted(APPLICATION_ID).await); + + clear_data_store_temp_dir(temp_dir).await; + + + } + }