From 15494c8efb15fde2826f935ba487daa62495ccdc Mon Sep 17 00:00:00 2001 From: reddevilmidzy Date: Sun, 3 Aug 2025 01:26:54 +0900 Subject: [PATCH 1/6] refactor: Translate Korean comments to English in git.rs --- src/file_manager/git.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/file_manager/git.rs b/src/file_manager/git.rs index f679ba3..0ef385b 100644 --- a/src/file_manager/git.rs +++ b/src/file_manager/git.rs @@ -322,12 +322,12 @@ mod tests { let statuses = repo.statuses(None).unwrap(); - // 워킹 디렉토리에 존재하지 않아야 한다. + // Should not exist in working directory assert!(!statuses.iter().any(|e| e.status().is_wt_new())); assert!(!statuses.iter().any(|e| e.status().is_wt_modified())); assert!(!statuses.iter().any(|e| e.status().is_wt_deleted())); - // 스테이징 영역에 올라와야 한다. + // Should be in staging area assert!(statuses.iter().all(|e| e.status().is_index_new())); fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; From b8e27c23caa5611aa94f0a4ae58c2a2704c01603 Mon Sep 17 00:00:00 2001 From: reddevilmidzy Date: Sun, 3 Aug 2025 01:48:28 +0900 Subject: [PATCH 2/6] test: Use tempfile for automatic test cleanup --- Cargo.toml | 1 + src/file_manager/git.rs | 216 ++++++++++++++++++++++++++++++++-------- 2 files changed, 173 insertions(+), 44 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a4c09fb..3f40ae1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,4 @@ uuid = { version = "1.17.0", features = ["v4"] } [dev-dependencies] reqwest = { version = "0.12.19", features = ["json", "rustls-tls"] } rstest = "0.25.0" +tempfile = "3.20" diff --git a/src/file_manager/git.rs b/src/file_manager/git.rs index 0ef385b..b0e9a48 100644 --- a/src/file_manager/git.rs +++ b/src/file_manager/git.rs @@ -11,15 +11,34 @@ const DEFAULT_DIRECTORIES: [&str; 3] = ["solutions", "tests", "statements"]; #[derive(Debug)] struct GitManager { problem_id: u32, + base_path: Option, } impl GitManager { fn new(problem_id: u32) -> Self { - Self { problem_id } + Self { + problem_id, + base_path: None, + } + } + + fn new_with_path(problem_id: u32, base_path: PathBuf) -> Self { + Self { + problem_id, + base_path: Some(base_path), + } + } + + fn get_upload_path(&self) -> PathBuf { + if let Some(ref base_path) = self.base_path { + base_path.join(self.problem_id.to_string()) + } else { + PathBuf::from(UPLOAD_DIR).join(self.problem_id.to_string()) + } } fn git_init(&self) -> Result<()> { - let path = PathBuf::from(UPLOAD_DIR).join(self.problem_id.to_string()); + let path = self.get_upload_path(); Repository::init(&path) .and_then(|repo| { let mut config = repo.config()?; @@ -28,7 +47,7 @@ impl GitManager { Ok(repo) }) .map(|_| ()) - .with_context(|| format!("Failed to init git repo at {:?}", path)) + .with_context(|| format!("Failed to init git repo at {path:?}")) } async fn create_problem(&self) -> Result<()> { @@ -127,12 +146,12 @@ impl GitManager { } async fn create_default_directories(&self) -> Result<()> { - let base_path = PathBuf::from(UPLOAD_DIR).join(self.problem_id.to_string()); + let base_path = self.get_upload_path(); for dir in DEFAULT_DIRECTORIES { let path = base_path.join(dir); fs::create_dir_all(path) .await - .with_context(|| format!("Failed to create directory: {}", dir))?; + .with_context(|| format!("Failed to create directory: {dir}"))?; } Ok(()) } @@ -169,9 +188,9 @@ impl GitManager { } fn get_repository(&self) -> Result { - let path = PathBuf::from(UPLOAD_DIR).join(self.problem_id.to_string()); + let path = self.get_upload_path(); Repository::open(&path) - .with_context(|| format!("Failed to open git repository at {:?}", path)) + .with_context(|| format!("Failed to open git repository at {path:?}")) } } @@ -194,6 +213,7 @@ mod tests { use super::*; use rstest::rstest; use std::path::Path; + use tempfile::TempDir; use tokio::fs; #[rstest] @@ -223,20 +243,24 @@ mod tests { #[tokio::test] async fn can_init_git_repository() -> Result<(), std::io::Error> { + let temp_dir = TempDir::new()?; let problem_id = 10; - let git_manager = GitManager::new(problem_id); + let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); assert!(git_manager.git_init().is_ok()); - assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}").as_str()).exists()); - assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/.git").as_str()).exists()); + assert!(Path::new(git_manager.get_upload_path().to_str().unwrap()).exists()); + assert!(Path::new( + format!("{}/.git", git_manager.get_upload_path().to_str().unwrap()).as_str() + ) + .exists()); - fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; Ok(()) } #[tokio::test] async fn can_set_config() -> Result<(), std::io::Error> { + let temp_dir = TempDir::new()?; let problem_id = 11; - let git_manager = GitManager::new(problem_id); + let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); assert!(git_manager.git_init().is_ok()); let repo = git_manager.get_repository().unwrap(); let config = repo.config().unwrap(); @@ -246,48 +270,103 @@ mod tests { Ok("admin@coduck.com".to_string()) ); - fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; Ok(()) } #[tokio::test] async fn can_create_default_file() -> Result<(), std::io::Error> { + let temp_dir = TempDir::new()?; let problem_id = 12; - let git_manager = GitManager::new(problem_id); + let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); assert!(git_manager.create_default_directories().await.is_ok()); - assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/solutions").as_str()).exists()); - assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/tests").as_str()).exists()); - assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/statements").as_str()).exists()); + assert!(Path::new( + format!( + "{}/solutions", + git_manager.get_upload_path().to_str().unwrap() + ) + .as_str() + ) + .exists()); + assert!(Path::new( + format!("{}/tests", git_manager.get_upload_path().to_str().unwrap()).as_str() + ) + .exists()); + assert!(Path::new( + format!( + "{}/statements", + git_manager.get_upload_path().to_str().unwrap() + ) + .as_str() + ) + .exists()); - fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; Ok(()) } #[tokio::test] async fn can_create_problem() -> Result<(), std::io::Error> { + let temp_dir = TempDir::new()?; let problem_id = 13; - let git_manager = GitManager::new(problem_id); + let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); assert!(git_manager.create_problem().await.is_ok()); - assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}").as_str()).exists()); - assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/.git").as_str()).exists()); - assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/solutions").as_str()).exists()); - assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/tests").as_str()).exists()); - assert!(Path::new(format!("{UPLOAD_DIR}/{problem_id}/statements").as_str()).exists()); + assert!(Path::new(git_manager.get_upload_path().to_str().unwrap()).exists()); + assert!(Path::new( + format!("{}/.git", git_manager.get_upload_path().to_str().unwrap()).as_str() + ) + .exists()); + assert!(Path::new( + format!( + "{}/solutions", + git_manager.get_upload_path().to_str().unwrap() + ) + .as_str() + ) + .exists()); + assert!(Path::new( + format!("{}/tests", git_manager.get_upload_path().to_str().unwrap()).as_str() + ) + .exists()); + assert!(Path::new( + format!( + "{}/statements", + git_manager.get_upload_path().to_str().unwrap() + ) + .as_str() + ) + .exists()); - fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; Ok(()) } #[tokio::test] async fn can_get_git_status() -> Result<(), tokio::io::Error> { + let temp_dir = TempDir::new()?; let problem_id = 14; - let git_manager = GitManager::new(problem_id); + let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); git_manager.git_init().unwrap(); - fs::create_dir_all(format!("{UPLOAD_DIR}/{problem_id}/tests")).await?; - fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.in"), "1 2").await?; - fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.out"), "3").await?; + fs::create_dir_all(format!( + "{}/tests", + git_manager.get_upload_path().to_str().unwrap() + )) + .await?; + fs::write( + format!( + "{}/tests/1.in", + git_manager.get_upload_path().to_str().unwrap() + ), + "1 2", + ) + .await?; + fs::write( + format!( + "{}/tests/1.out", + git_manager.get_upload_path().to_str().unwrap() + ), + "3", + ) + .await?; let file_infos = git_manager.git_status().unwrap(); let expected = vec![ @@ -302,21 +381,39 @@ mod tests { ]; assert_eq!(file_infos, expected); - fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; Ok(()) } #[tokio::test] async fn can_git_add() -> Result<(), tokio::io::Error> { + let temp_dir = TempDir::new()?; let problem_id = 15; - let git_manager = GitManager::new(problem_id); + let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); git_manager.git_init().unwrap(); let repo = git_manager.get_repository().unwrap(); - fs::create_dir_all(format!("{UPLOAD_DIR}/{problem_id}/tests")).await?; - fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.in"), "1 2").await?; - fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.out"), "3").await?; + fs::create_dir_all(format!( + "{}/tests", + git_manager.get_upload_path().to_str().unwrap() + )) + .await?; + fs::write( + format!( + "{}/tests/1.in", + git_manager.get_upload_path().to_str().unwrap() + ), + "1 2", + ) + .await?; + fs::write( + format!( + "{}/tests/1.out", + git_manager.get_upload_path().to_str().unwrap() + ), + "3", + ) + .await?; assert!(git_manager.git_add_all().is_ok()); @@ -330,19 +427,37 @@ mod tests { // Should be in staging area assert!(statuses.iter().all(|e| e.status().is_index_new())); - fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; Ok(()) } #[tokio::test] async fn can_commit() -> Result<(), tokio::io::Error> { + let temp_dir = TempDir::new()?; let problem_id = 16; - let git_manager = GitManager::new(problem_id); + let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); git_manager.git_init().unwrap(); - fs::create_dir_all(format!("{UPLOAD_DIR}/{problem_id}/tests")).await?; - fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.in"), "1 2").await?; - fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.out"), "3").await?; + fs::create_dir_all(format!( + "{}/tests", + git_manager.get_upload_path().to_str().unwrap() + )) + .await?; + fs::write( + format!( + "{}/tests/1.in", + git_manager.get_upload_path().to_str().unwrap() + ), + "1 2", + ) + .await?; + fs::write( + format!( + "{}/tests/1.out", + git_manager.get_upload_path().to_str().unwrap() + ), + "3", + ) + .await?; let commit_message = "add test 1"; @@ -354,19 +469,33 @@ mod tests { assert_eq!(commit.message(), Some(commit_message)); - fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; Ok(()) } #[tokio::test] async fn can_get_log() -> Result<(), tokio::io::Error> { + let temp_dir = TempDir::new()?; let problem_id = 17; - let git_manager = GitManager::new(problem_id); + let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); git_manager.git_init().unwrap(); git_manager.create_default_directories().await.unwrap(); - fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.in"), "1 2").await?; - fs::write(format!("{UPLOAD_DIR}/{problem_id}/tests/1.out"), "3").await?; + fs::write( + format!( + "{}/tests/1.in", + git_manager.get_upload_path().to_str().unwrap() + ), + "1 2", + ) + .await?; + fs::write( + format!( + "{}/tests/1.out", + git_manager.get_upload_path().to_str().unwrap() + ), + "3", + ) + .await?; git_manager .git_commit("create default file".to_string()) @@ -386,7 +515,6 @@ mod tests { ]; assert_eq!(log[0].paths, expected_path); - fs::remove_dir_all(format!("{UPLOAD_DIR}/{problem_id}")).await?; Ok(()) } } From 2e2178f8f98d4cb7463505f11bdc2f7323cd6355 Mon Sep 17 00:00:00 2001 From: reddevilmidzy Date: Sat, 20 Sep 2025 15:51:49 +0900 Subject: [PATCH 3/6] refactor: Improve test isolation and dependency injection - Replace per-request service creation with dependency injection - Add .env support for UPLOAD_DIR configuration - Implement sequential test execution to prevent race conditions - Auto-generate test problem IDs using rand --- .env | 1 + Cargo.toml | 3 + src/file_manager/git.rs | 53 ++--- src/file_manager/handlers.rs | 395 +++++++++++++-------------------- src/file_manager/mod.rs | 4 + src/file_manager/service.rs | 148 ++++++++++++ src/file_manager/storage.rs | 78 +++++++ src/lib.rs | 22 +- tests/file_manager/handlers.rs | 113 +++++----- 9 files changed, 496 insertions(+), 321 deletions(-) create mode 100644 src/file_manager/service.rs create mode 100644 src/file_manager/storage.rs diff --git a/.env b/.env index ccac3d1..144ae6e 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ DATABASE_URL="postgres://postgres:password@localhost:5432/coduck" +UPLOAD_DIR="uploads" diff --git a/Cargo.toml b/Cargo.toml index 3f40ae1..d1b7a01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ name = "coduck-backend" anyhow = "1.0" axum = { version = "0.8.4", features = ["json", "multipart"] } chrono = { version = "0.4.38", features = ["serde"] } +dotenv = "0.15.0" git2 = "0.20.2" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.133" @@ -21,6 +22,8 @@ tokio = { version = "1.45.1", features = ["full"] } uuid = { version = "1.17.0", features = ["v4"] } [dev-dependencies] +rand = "0.9" reqwest = { version = "0.12.19", features = ["json", "rustls-tls"] } rstest = "0.25.0" +serial_test = "3.2" tempfile = "3.20" diff --git a/src/file_manager/git.rs b/src/file_manager/git.rs index b0e9a48..1faa0f5 100644 --- a/src/file_manager/git.rs +++ b/src/file_manager/git.rs @@ -5,36 +5,24 @@ use git2::{DiffOptions, IndexAddOption, Repository, StatusOptions, Time}; use std::path::PathBuf; use tokio::fs; -const UPLOAD_DIR: &str = "uploads"; const DEFAULT_DIRECTORIES: [&str; 3] = ["solutions", "tests", "statements"]; #[derive(Debug)] struct GitManager { problem_id: u32, - base_path: Option, + base_path: PathBuf, } impl GitManager { - fn new(problem_id: u32) -> Self { + fn new(problem_id: u32, base_path: PathBuf) -> Self { Self { problem_id, - base_path: None, - } - } - - fn new_with_path(problem_id: u32, base_path: PathBuf) -> Self { - Self { - problem_id, - base_path: Some(base_path), + base_path, } } fn get_upload_path(&self) -> PathBuf { - if let Some(ref base_path) = self.base_path { - base_path.join(self.problem_id.to_string()) - } else { - PathBuf::from(UPLOAD_DIR).join(self.problem_id.to_string()) - } + self.base_path.join(self.problem_id.to_string()) } fn git_init(&self) -> Result<()> { @@ -211,6 +199,7 @@ struct FileInfo { #[cfg(test)] mod tests { use super::*; + use rand::Rng; use rstest::rstest; use std::path::Path; use tempfile::TempDir; @@ -244,8 +233,8 @@ mod tests { #[tokio::test] async fn can_init_git_repository() -> Result<(), std::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = 10; - let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); + let problem_id = rand::rng().random_range(0..=u32::MAX); + let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); assert!(git_manager.git_init().is_ok()); assert!(Path::new(git_manager.get_upload_path().to_str().unwrap()).exists()); assert!(Path::new( @@ -259,8 +248,8 @@ mod tests { #[tokio::test] async fn can_set_config() -> Result<(), std::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = 11; - let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); + let problem_id = rand::rng().random_range(0..=u32::MAX); + let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); assert!(git_manager.git_init().is_ok()); let repo = git_manager.get_repository().unwrap(); let config = repo.config().unwrap(); @@ -276,8 +265,8 @@ mod tests { #[tokio::test] async fn can_create_default_file() -> Result<(), std::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = 12; - let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); + let problem_id = rand::rng().random_range(0..=u32::MAX); + let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); assert!(git_manager.create_default_directories().await.is_ok()); assert!(Path::new( format!( @@ -306,8 +295,8 @@ mod tests { #[tokio::test] async fn can_create_problem() -> Result<(), std::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = 13; - let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); + let problem_id = rand::rng().random_range(0..=u32::MAX); + let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); assert!(git_manager.create_problem().await.is_ok()); assert!(Path::new(git_manager.get_upload_path().to_str().unwrap()).exists()); assert!(Path::new( @@ -341,8 +330,8 @@ mod tests { #[tokio::test] async fn can_get_git_status() -> Result<(), tokio::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = 14; - let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); + let problem_id = rand::rng().random_range(0..=u32::MAX); + let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); git_manager.git_init().unwrap(); @@ -387,8 +376,8 @@ mod tests { #[tokio::test] async fn can_git_add() -> Result<(), tokio::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = 15; - let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); + let problem_id = rand::rng().random_range(0..=u32::MAX); + let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); git_manager.git_init().unwrap(); @@ -433,8 +422,8 @@ mod tests { #[tokio::test] async fn can_commit() -> Result<(), tokio::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = 16; - let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); + let problem_id = rand::rng().random_range(0..=u32::MAX); + let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); git_manager.git_init().unwrap(); fs::create_dir_all(format!( @@ -475,8 +464,8 @@ mod tests { #[tokio::test] async fn can_get_log() -> Result<(), tokio::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = 17; - let git_manager = GitManager::new_with_path(problem_id, temp_dir.path().to_path_buf()); + let problem_id = rand::rng().random_range(0..=u32::MAX); + let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); git_manager.git_init().unwrap(); git_manager.create_default_directories().await.unwrap(); diff --git a/src/file_manager/handlers.rs b/src/file_manager/handlers.rs index d0e9a6c..f7d4368 100644 --- a/src/file_manager/handlers.rs +++ b/src/file_manager/handlers.rs @@ -1,29 +1,45 @@ -use crate::file_manager::{ - FileMetadata, Language, UpdateFileContentRequest, UpdateFilenameRequest, -}; +use crate::file_manager::{FileService, UpdateFileContentRequest, UpdateFilenameRequest}; use anyhow::{anyhow, Result}; use axum::{ - extract::{Multipart, Path}, + extract::{Multipart, Path, State}, http::StatusCode, - response::IntoResponse, + response::{IntoResponse, Response}, Json, }; -use chrono::Utc; -use std::path::PathBuf; -use tokio::fs; -use uuid::Uuid; +use std::sync::Arc; + +async fn extract_multipart_data(mut multipart: Multipart) -> Result<(String, axum::body::Bytes)> { + let field = multipart + .next_field() + .await? + .ok_or_else(|| anyhow!("Missing multipart field"))?; + + let filename = field + .file_name() + .ok_or_else(|| anyhow!("Missing filename in multipart field"))? + .to_string(); -const UPLOAD_DIR: &str = "uploads"; + let data = field.bytes().await?; + + Ok((filename, data)) +} -async fn file_exists(file_path: &PathBuf) -> bool { - tokio::fs::metadata(file_path).await.is_ok() +fn handle_error(error: anyhow::Error) -> Response { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({ + "error": error.to_string() + })), + ) + .into_response() } pub async fn upload_file( + State(file_service): State>, Path((problem_id, category)): Path<(u32, String)>, multipart: Multipart, -) -> impl IntoResponse { +) -> Response { let (filename, data) = match extract_multipart_data(multipart).await { Ok(data) => data, Err(e) => { @@ -37,293 +53,202 @@ pub async fn upload_file( } }; - let file_id = Uuid::new_v4().to_string(); - let upload_dir = PathBuf::from(UPLOAD_DIR) - .join(problem_id.to_string()) - .join(&category); - - let file_path = upload_dir.join(&filename); - - if file_exists(&file_path).await { - return ( - StatusCode::CONFLICT, - Json(serde_json::json!({ - "error": format!("File '{filename}' already exists in this category") - })), - ) - .into_response(); - } - - let now = Utc::now() - .timestamp_nanos_opt() - .expect("Failed to get timestamp"); - let language = match Language::from_filename(&filename) { - Ok(lang) => lang, + match file_service + .upload_file(problem_id, category, filename, data.to_vec()) + .await + { + Ok(metadata) => (StatusCode::CREATED, Json(metadata)).into_response(), Err(e) => { - return ( - StatusCode::BAD_REQUEST, - Json(serde_json::json!({ - "error": e.to_string() - })), - ) - .into_response(); + if e.to_string().contains("already exists") { + ( + StatusCode::CONFLICT, + Json(serde_json::json!({ + "error": e.to_string() + })), + ) + .into_response() + } else { + ( + StatusCode::BAD_REQUEST, + Json(serde_json::json!({ + "error": e.to_string() + })), + ) + .into_response() + } } - }; - - if let Err(e) = save_file(&upload_dir, &file_path, &data).await { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "error": e.to_string() - })), - ) - .into_response(); } - - let metadata = FileMetadata { - id: file_id, - filename: filename, - language: language, - category: category, - size: data.len() as u64, - created_at: now, - updated_at: now, - }; - (StatusCode::CREATED, Json(metadata)).into_response() -} - -async fn save_file( - upload_dir: &PathBuf, - file_path: &PathBuf, - data: &axum::body::Bytes, -) -> Result<()> { - fs::create_dir_all(upload_dir) - .await - .map_err(|_| anyhow!("Failed to create directory"))?; - - fs::write(file_path, data) - .await - .map_err(|_| anyhow!("Failed to write file"))?; - - Ok(()) -} - -async fn extract_multipart_data(mut multipart: Multipart) -> Result<(String, axum::body::Bytes)> { - let field = multipart - .next_field() - .await? - .ok_or_else(|| anyhow!("Missing multipart field"))?; - - let filename = field - .file_name() - .ok_or_else(|| anyhow!("Missing filename in multipart field"))? - .to_string(); - - let data = field.bytes().await?; - - Ok((filename, data)) } pub async fn get_file( + State(file_service): State>, Path((problem_id, category, filename)): Path<(u32, String, String)>, -) -> impl IntoResponse { - let file_path = PathBuf::from(UPLOAD_DIR) - .join(problem_id.to_string()) - .join(&category) - .join(&filename); - - if !file_exists(&file_path).await { - return ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ - "error": format!("File '{filename}' not found in category '{category}'") - })), - ) - .into_response(); - } - - match fs::read(&file_path).await { +) -> Response { + match file_service.get_file(problem_id, category, filename).await { Ok(content) => { let content_str = String::from_utf8_lossy(&content); - ( StatusCode::OK, - [("Content-Type", "text/html; charset=UTF-8")], + [("Content-Type", "text/plain; charset=UTF-8")], content_str.to_string(), ) .into_response() } - Err(_) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "error": "Failed to read file content" - })), - ) - .into_response(), + Err(e) => { + if e.to_string().contains("not found") { + ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ + "error": e.to_string() + })), + ) + .into_response() + } else { + handle_error(e) + } + } } } pub async fn get_files_by_category( + State(file_service): State>, Path((problem_id, category)): Path<(u32, String)>, -) -> impl IntoResponse { - let category_dir = PathBuf::from(UPLOAD_DIR) - .join(problem_id.to_string()) - .join(&category); - - if !file_exists(&category_dir).await { - return ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ - "error": format!("Category '{category}' not found for problem {problem_id}") - })), - ) - .into_response(); - } - - let mut files = Vec::new(); - - match fs::read_dir(&category_dir).await { - Ok(mut entries) => { - while let Ok(Some(entry)) = entries.next_entry().await { - let path = entry.path(); - if path.is_file() { - if let Some(filename) = path.file_name().and_then(|f| f.to_str()) { - files.push(filename.to_string()); - } - } +) -> Response { + match file_service + .get_files_by_category(problem_id, category) + .await + { + Ok(files) => (StatusCode::OK, Json(files)).into_response(), + Err(e) => { + if e.to_string().contains("not found") { + ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ + "error": e.to_string() + })), + ) + .into_response() + } else { + handle_error(e) } } - Err(_) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "error": "Failed to read directory" - })), - ) - .into_response(); - } } - - (StatusCode::OK, Json(files)).into_response() } pub async fn delete_file( + State(file_service): State>, Path((problem_id, category, filename)): Path<(u32, String, String)>, -) -> impl IntoResponse { - let file_path = PathBuf::from(UPLOAD_DIR) - .join(problem_id.to_string()) - .join(&category) - .join(&filename); - - if !file_exists(&file_path).await { - return ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ - "error": format!("File '{filename}' not found in category '{category}'") - })), - ) - .into_response(); - } - - match fs::remove_file(&file_path).await { +) -> Response { + match file_service + .delete_file(problem_id, category, filename) + .await + { Ok(_) => ( StatusCode::OK, Json(serde_json::json!({ - "message": format!("File '{filename}' deleted successfully") - })), - ) - .into_response(), - Err(_) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "error": "Failed to delete file" + "message": "File deleted successfully" })), ) .into_response(), + Err(e) => { + if e.to_string().contains("not found") { + ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ + "error": e.to_string() + })), + ) + .into_response() + } else { + handle_error(e) + } + } } } pub async fn update_file_content( + State(file_service): State>, Path((problem_id, category, filename)): Path<(u32, String, String)>, Json(update_request): Json, -) -> impl IntoResponse { - let file_path = PathBuf::from(UPLOAD_DIR) - .join(problem_id.to_string()) - .join(&category) - .join(&filename); - - if !file_exists(&file_path).await { +) -> Response { + if update_request.content.is_empty() { return ( - StatusCode::NOT_FOUND, + StatusCode::UNPROCESSABLE_ENTITY, Json(serde_json::json!({ - "error": format!("File '{filename}' not found in category '{category}'") + "error": "Content cannot be empty" })), ) .into_response(); } - match fs::write(&file_path, &update_request.content).await { + match file_service + .update_file_content(problem_id, category, filename, update_request.content) + .await + { Ok(_) => ( StatusCode::OK, Json(serde_json::json!({ - "message": format!("File '{filename}' updated successfully") - })), - ) - .into_response(), - Err(_) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "error": "Failed to update file" + "message": "File content updated successfully" })), ) .into_response(), + Err(e) => { + if e.to_string().contains("not found") { + ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ + "error": e.to_string() + })), + ) + .into_response() + } else { + handle_error(e) + } + } } } pub async fn update_filename( + State(file_service): State>, Path((problem_id, category)): Path<(u32, String)>, Json(update_request): Json, -) -> impl IntoResponse { - let file_path = PathBuf::from(UPLOAD_DIR) - .join(problem_id.to_string()) - .join(&category) - .join(&update_request.old_filename); - - if !file_exists(&file_path).await { - return ( - StatusCode::NOT_FOUND, - Json(serde_json::json!({ - "error": format!( - "File '{}' not found in category '{category}'", - update_request.old_filename - ) - })), +) -> Response { + match file_service + .update_filename( + problem_id, + category, + update_request.old_filename, + update_request.new_filename, ) - .into_response(); - } - - match fs::rename( - &file_path, - &file_path.with_file_name(&update_request.new_filename), - ) - .await + .await { Ok(_) => ( StatusCode::OK, Json(serde_json::json!({ - "message": format!( - "File '{}' updated successfully to '{}'", - update_request.old_filename, update_request.new_filename - ) - })), - ) - .into_response(), - Err(_) => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ - "error": "Failed to update filename" + "message": "Filename updated successfully" })), ) .into_response(), + Err(e) => { + if e.to_string().contains("not found") { + ( + StatusCode::NOT_FOUND, + Json(serde_json::json!({ + "error": e.to_string() + })), + ) + .into_response() + } else if e.to_string().contains("already exists") { + ( + StatusCode::CONFLICT, + Json(serde_json::json!({ + "error": e.to_string() + })), + ) + .into_response() + } else { + handle_error(e) + } + } } } diff --git a/src/file_manager/mod.rs b/src/file_manager/mod.rs index b4ad472..b9b92aa 100644 --- a/src/file_manager/mod.rs +++ b/src/file_manager/mod.rs @@ -1,6 +1,10 @@ mod git; mod handlers; mod models; +mod service; +mod storage; pub(crate) use handlers::*; pub use models::*; +pub use service::*; +pub use storage::*; diff --git a/src/file_manager/service.rs b/src/file_manager/service.rs new file mode 100644 index 0000000..5adbfc2 --- /dev/null +++ b/src/file_manager/service.rs @@ -0,0 +1,148 @@ +use crate::file_manager::{FileMetadata, Language, LocalFileStorage}; +use anyhow::{anyhow, Result}; +use chrono::Utc; +use uuid::Uuid; + +pub struct FileService { + storage: LocalFileStorage, +} + +impl FileService { + pub fn new(storage: LocalFileStorage) -> Self { + Self { storage } + } + + pub async fn upload_file( + &self, + problem_id: u32, + category: String, + filename: String, + data: Vec, + ) -> Result { + let file_path = format!("{}/{}/{}", problem_id, category, filename); + + if self.storage.file_exists(&file_path).await { + return Err(anyhow!("File '{filename}' already exists in this category",)); + } + + let language = + Language::from_filename(&filename).map_err(|e| anyhow!("Invalid filename: {e}"))?; + + self.storage.save_file(&file_path, &data).await?; + + let now = Utc::now() + .timestamp_nanos_opt() + .ok_or(anyhow!("Failed to get timestamp"))?; + + let metadata = FileMetadata { + id: Uuid::new_v4().to_string(), + filename, + language, + category, + size: data.len() as u64, + created_at: now, + updated_at: now, + }; + + Ok(metadata) + } + + pub async fn get_file( + &self, + problem_id: u32, + category: String, + filename: String, + ) -> Result> { + let file_path = format!("{}/{}/{}", problem_id, category, filename); + + if !self.storage.file_exists(&file_path).await { + return Err(anyhow!( + "File '{filename}' not found in category '{category}'" + )); + } + + self.storage.read_file(&file_path).await + } + + pub async fn get_files_by_category( + &self, + problem_id: u32, + category: String, + ) -> Result> { + let category_path = format!("{}/{}", problem_id, category); + + if !self.storage.file_exists(&category_path).await { + return Err(anyhow!( + "Category '{category}' not found for problem {problem_id}" + )); + } + + self.storage.list_files(&category_path).await + } + + pub async fn delete_file( + &self, + problem_id: u32, + category: String, + filename: String, + ) -> Result<()> { + let file_path = format!("{}/{}/{}", problem_id, category, filename); + + if !self.storage.file_exists(&file_path).await { + return Err(anyhow!( + "File '{filename}' not found in category '{category}'" + )); + } + + self.storage.delete_file(&file_path).await + } + + pub async fn update_file_content( + &self, + problem_id: u32, + category: String, + filename: String, + content: String, + ) -> Result<()> { + let file_path = format!("{}/{}/{}", problem_id, category, filename); + + if !self.storage.file_exists(&file_path).await { + return Err(anyhow!( + "File '{filename}' not found in category '{category}'" + )); + } + + self.storage.save_file(&file_path, content.as_bytes()).await + } + + pub async fn update_filename( + &self, + problem_id: u32, + category: String, + old_filename: String, + new_filename: String, + ) -> Result<()> { + let old_path = format!("{}/{}/{}", problem_id, category, old_filename); + let new_path = format!("{}/{}/{}", problem_id, category, new_filename); + + if !self.storage.file_exists(&old_path).await { + return Err(anyhow!( + "File '{old_filename}' not found in category '{category}'" + )); + } + + if self.storage.file_exists(&new_path).await { + return Err(anyhow!( + "File '{new_filename}' already exists in category '{category}'" + )); + } + + let content = self.storage.read_file(&old_path).await?; + + self.storage.save_file(&new_path, &content).await?; + + self.storage.delete_file(&old_path).await?; + + Ok(()) + } +} diff --git a/src/file_manager/storage.rs b/src/file_manager/storage.rs new file mode 100644 index 0000000..df12253 --- /dev/null +++ b/src/file_manager/storage.rs @@ -0,0 +1,78 @@ +use anyhow::{anyhow, Result}; +use std::path::PathBuf; +use tokio::fs; + +pub struct LocalFileStorage { + base_path: PathBuf, +} + +impl LocalFileStorage { + pub fn new(base_path: PathBuf) -> Self { + Self { base_path } + } + + fn get_full_path(&self, path: &str) -> PathBuf { + self.base_path.join(path) + } + + pub async fn save_file(&self, path: &str, data: &[u8]) -> Result<()> { + let full_path = self.get_full_path(path); + let dir = full_path.parent().ok_or_else(|| anyhow!("Invalid path"))?; + + fs::create_dir_all(dir) + .await + .map_err(|_| anyhow!("Failed to create directory"))?; + + fs::write(&full_path, data) + .await + .map_err(|_| anyhow!("Failed to write file"))?; + + Ok(()) + } + + pub async fn read_file(&self, path: &str) -> Result> { + let full_path = self.get_full_path(path); + fs::read(&full_path) + .await + .map_err(|_| anyhow!("Failed to read file")) + } + + pub async fn delete_file(&self, path: &str) -> Result<()> { + let full_path = self.get_full_path(path); + fs::remove_file(&full_path) + .await + .map_err(|_| anyhow!("Failed to delete file")) + } + + pub async fn list_files(&self, path: &str) -> Result> { + let full_path = self.get_full_path(path); + let mut files = Vec::new(); + + let mut entries = fs::read_dir(&full_path) + .await + .map_err(|e| anyhow!("Failed to read directory: {}", e))?; + + while let Some(entry) = entries.next_entry().await? { + if entry.file_type().await?.is_file() { + let path = entry.path(); + if let Some(filename) = path.file_name().and_then(|f| f.to_str()) { + files.push(filename.to_string()); + } + } + } + + Ok(files) + } + + pub async fn file_exists(&self, path: &str) -> bool { + let full_path = self.get_full_path(path); + fs::metadata(&full_path).await.is_ok() + } + + pub async fn create_directory(&self, path: &str) -> Result<()> { + let full_path = self.get_full_path(path); + fs::create_dir_all(&full_path) + .await + .map_err(|_| anyhow!("Failed to create directory")) + } +} diff --git a/src/lib.rs b/src/lib.rs index 2c3e91d..2bab672 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,9 +5,12 @@ use axum::{ routing::{delete, get, post, put}, Router, }; +use std::path::PathBuf; +use std::sync::Arc; use crate::file_manager::{ - delete_file, get_file, get_files_by_category, update_file_content, update_filename, upload_file, + delete_file, get_file, get_files_by_category, update_file_content, update_filename, + upload_file, FileService, LocalFileStorage, }; async fn health_check() -> &'static str { @@ -15,6 +18,22 @@ async fn health_check() -> &'static str { } pub fn build_router() -> Router { + dotenv::dotenv().ok(); + + let upload_dir = std::env::var("UPLOAD_DIR").expect("UPLOAD_DIR must be set"); + let upload_path = PathBuf::from(upload_dir); + + if !upload_path.exists() { + std::fs::create_dir_all(&upload_path).expect("Failed to create upload directory"); + } + + if !upload_path.is_dir() { + panic!("UPLOAD_DIR must point to a directory"); + } + + let storage = LocalFileStorage::new(upload_path); + let file_service = Arc::new(FileService::new(storage)); + let problems_router = Router::new() .route("/{problem_id}/{category}", post(upload_file)) .route("/{problem_id}/{category}/{filename}", get(get_file)) @@ -29,4 +48,5 @@ pub fn build_router() -> Router { Router::new() .route("/health", get(health_check)) .nest("/problems", problems_router) + .with_state(file_service) } diff --git a/tests/file_manager/handlers.rs b/tests/file_manager/handlers.rs index a8a3861..b350197 100644 --- a/tests/file_manager/handlers.rs +++ b/tests/file_manager/handlers.rs @@ -1,4 +1,6 @@ +use rand::Rng; use std::path::Path; +use tempfile::TempDir; use tokio::fs; struct TestSetup { @@ -7,14 +9,14 @@ struct TestSetup { filename: String, multipart_body: String, port: u16, + temp_dir: TempDir, } -async fn setup_test(problem_id: u32, file_content: &[u8], filename: &str) -> TestSetup { - // 테스트용 디렉토리만 정리 (uploads 전체가 아닌 특정 problem_id만) - let test_upload_dir = format!("uploads/{}", problem_id); - if Path::new(&test_upload_dir).exists() { - fs::remove_dir_all(&test_upload_dir).await.unwrap(); - } +async fn setup_test(file_content: &[u8], filename: &str) -> TestSetup { + let problem_id = rand::rng().random_range(0..=u32::MAX); + let temp_dir = TempDir::new().unwrap(); + + std::env::set_var("UPLOAD_DIR", temp_dir.path().to_str().unwrap()); let multipart_body = format!( "--boundary\r\n\ @@ -35,12 +37,15 @@ async fn setup_test(problem_id: u32, file_content: &[u8], filename: &str) -> Tes .expect("Server failed to start"); }); + // Wait briefly to give the server time to start up + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; TestSetup { problem_id, file_content: file_content.to_vec(), filename: filename.to_string(), multipart_body, port, + temp_dir, } } @@ -58,21 +63,10 @@ async fn upload_file_request(setup: &TestSetup) -> reqwest::Response { .unwrap() } -async fn cleanup_test(problem_id: u32) { - let test_upload_dir = format!("uploads/{}", problem_id); - if Path::new(&test_upload_dir).exists() { - fs::remove_dir_all(&test_upload_dir).await.unwrap(); - } -} - #[tokio::test] +#[serial_test::serial] async fn upload_file_success() { - let setup = setup_test( - 0, - b"print(int(input()) + int(input()))", - "aplusb-solution.py", - ) - .await; + let setup = setup_test(b"print(int(input()) + int(input()))", "aplusb-solution.py").await; let response = upload_file_request(&setup).await; @@ -88,18 +82,22 @@ async fn upload_file_success() { ); assert_eq!(response_json.category, "solution"); - let expected_file_path = format!("uploads/{}/solution/{}", setup.problem_id, setup.filename); + let expected_file_path = format!( + "{}/{}/solution/{}", + setup.temp_dir.path().to_str().unwrap(), + setup.problem_id, + setup.filename + ); assert!(Path::new(&expected_file_path).exists()); let saved_content = fs::read(&expected_file_path).await.unwrap(); assert_eq!(saved_content, setup.file_content); - - cleanup_test(setup.problem_id).await; } #[tokio::test] +#[serial_test::serial] async fn upload_file_handles_duplicate_filename() { - let setup = setup_test(1, b"print('Hello, World!')", "duplicate-test.py").await; + let setup = setup_test(b"print('Hello, World!')", "duplicate-test.py").await; // 첫 번째 파일 업로드 (성공) let response1 = upload_file_request(&setup).await; @@ -119,16 +117,19 @@ async fn upload_file_handles_duplicate_filename() { .contains("already exists")); // 파일이 실제로 하나만 존재하는지 확인 - let expected_file_path = format!("uploads/{}/solution/{}", setup.problem_id, setup.filename); + let expected_file_path = format!( + "{}/{}/solution/{}", + setup.temp_dir.path().to_str().unwrap(), + setup.problem_id, + setup.filename + ); assert!(Path::new(&expected_file_path).exists()); - - cleanup_test(setup.problem_id).await; } #[tokio::test] +#[serial_test::serial] async fn get_file_success() { let setup = setup_test( - 2, b"#include \nint main() { std::cout << \"Hello, World!\" << std::endl; return 0; }", "hello.cpp", ) @@ -168,13 +169,12 @@ async fn get_file_success() { file_content, "#include \nint main() { std::cout << \"Hello, World!\" << std::endl; return 0; }" ); - - cleanup_test(setup.problem_id).await; } #[tokio::test] +#[serial_test::serial] async fn get_files_by_category_success() { - let setup = setup_test(3, b"print('Hello, World!')", "hello.py").await; + let setup = setup_test(b"print('Hello, World!')", "hello.py").await; // 파일 업로드 let upload_response = upload_file_request(&setup).await; @@ -198,20 +198,24 @@ async fn get_files_by_category_success() { assert_eq!(files.len(), 1); assert_eq!(files[0], setup.filename); - - cleanup_test(setup.problem_id).await; } #[tokio::test] +#[serial_test::serial] async fn delete_file_success() { - let setup = setup_test(5, b"print('Hello, World!')", "delete-test.py").await; + let setup = setup_test(b"print('Hello, World!')", "delete-test.py").await; // 먼저 파일 업로드 let upload_response = upload_file_request(&setup).await; assert_eq!(upload_response.status(), reqwest::StatusCode::CREATED); // 파일이 존재하는지 확인 - let expected_file_path = format!("uploads/{}/solution/{}", setup.problem_id, setup.filename); + let expected_file_path = format!( + "{}/{}/solution/{}", + setup.temp_dir.path().to_str().unwrap(), + setup.problem_id, + setup.filename + ); assert!(Path::new(&expected_file_path).exists()); // 파일 삭제 @@ -238,13 +242,12 @@ async fn delete_file_success() { // 파일이 실제로 삭제되었는지 확인 assert!(!Path::new(&expected_file_path).exists()); - - cleanup_test(setup.problem_id).await; } #[tokio::test] +#[serial_test::serial] async fn delete_file_not_found() { - let setup = setup_test(6, b"print('Hello, World!')", "not-found-test.py").await; + let setup = setup_test(b"print('Hello, World!')", "not-found-test.py").await; // 파일을 업로드하지 않고 바로 삭제 시도 let client = reqwest::Client::new(); @@ -267,13 +270,12 @@ async fn delete_file_not_found() { .as_str() .unwrap() .contains("not found")); - - cleanup_test(setup.problem_id).await; } #[tokio::test] +#[serial_test::serial] async fn update_file_content_success() { - let setup = setup_test(7, b"print(int(input()) - int(input()))", "update-test.py").await; + let setup = setup_test(b"print(int(input()) - int(input()))", "update-test.py").await; // 먼저 파일 업로드 let upload_response = upload_file_request(&setup).await; @@ -307,17 +309,20 @@ async fn update_file_content_success() { .contains("updated successfully")); // 파일 내용이 실제로 업데이트되었는지 확인 - let expected_file_path = format!("uploads/{}/solution/{}", setup.problem_id, setup.filename); + let expected_file_path = format!( + "{}/{}/solution/{}", + setup.temp_dir.path().to_str().unwrap(), + setup.problem_id, + setup.filename + ); let updated_content = fs::read_to_string(&expected_file_path).await.unwrap(); assert_eq!(updated_content, "print(int(input()) + int(input()))"); - - cleanup_test(setup.problem_id).await; } #[tokio::test] +#[serial_test::serial] async fn update_file_not_found() { let setup = setup_test( - 8, b"print(int(input()) - int(input()))", "not-found-update-test.py", ) @@ -349,13 +354,12 @@ async fn update_file_not_found() { .as_str() .unwrap() .contains("not found")); - - cleanup_test(setup.problem_id).await; } #[tokio::test] +#[serial_test::serial] async fn update_file_missing_content() { - let setup = setup_test(9, b"print('Hello, World!')", "missing-content-test.py").await; + let setup = setup_test(b"print('Hello, World!')", "missing-content-test.py").await; // 파일 업로드 let upload_response = upload_file_request(&setup).await; @@ -380,12 +384,12 @@ async fn update_file_missing_content() { .unwrap(); assert_eq!(response.status(), reqwest::StatusCode::UNPROCESSABLE_ENTITY); - cleanup_test(setup.problem_id).await; } #[tokio::test] -async fn update_file_filename_success() { - let setup = setup_test(10, b"print(int(input()) + int(input()))", "aplusb.py").await; +#[serial_test::serial] +async fn update_filename_success() { + let setup = setup_test(b"print(int(input()) + int(input()))", "aplusb.py").await; // 먼저 파일 업로드 let upload_response = upload_file_request(&setup).await; @@ -420,8 +424,11 @@ async fn update_file_filename_success() { .contains("updated successfully")); // 파일 이름이 실제로 업데이트되었는지 확인 - let expected_file_path = format!("uploads/{}/solution/{}", setup.problem_id, new_filename); + let expected_file_path = format!( + "{}/{}/solution/{}", + setup.temp_dir.path().to_str().unwrap(), + setup.problem_id, + new_filename + ); assert!(Path::new(&expected_file_path).exists()); - - cleanup_test(setup.problem_id).await; } From 7656101053775bf8cc94533c06bd4e51fcd00501 Mon Sep 17 00:00:00 2001 From: reddevilmidzy Date: Sat, 20 Sep 2025 17:12:24 +0900 Subject: [PATCH 4/6] docs: Translate Korean comments to English in test files --- tests/file_manager/handlers.rs | 36 +++++++++++----------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/tests/file_manager/handlers.rs b/tests/file_manager/handlers.rs index b350197..a7e5cc6 100644 --- a/tests/file_manager/handlers.rs +++ b/tests/file_manager/handlers.rs @@ -99,14 +99,10 @@ async fn upload_file_success() { async fn upload_file_handles_duplicate_filename() { let setup = setup_test(b"print('Hello, World!')", "duplicate-test.py").await; - // 첫 번째 파일 업로드 (성공) let response1 = upload_file_request(&setup).await; assert_eq!(response1.status(), reqwest::StatusCode::CREATED); - // 두 번째 파일 업로드 (같은 파일명, 실패해야 함) let response2 = upload_file_request(&setup).await; - - // 중복 파일 에러 확인 assert_eq!(response2.status(), reqwest::StatusCode::CONFLICT); let error_response = response2.json::().await.unwrap(); @@ -116,7 +112,6 @@ async fn upload_file_handles_duplicate_filename() { .unwrap() .contains("already exists")); - // 파일이 실제로 하나만 존재하는지 확인 let expected_file_path = format!( "{}/{}/solution/{}", setup.temp_dir.path().to_str().unwrap(), @@ -135,11 +130,10 @@ async fn get_file_success() { ) .await; - // 먼저 파일 업로드 let upload_response = upload_file_request(&setup).await; assert_eq!(upload_response.status(), reqwest::StatusCode::CREATED); - // 파일 내용 조회 + // Retrieve file content let client = reqwest::Client::new(); let response = client .get(&format!( @@ -154,7 +148,6 @@ async fn get_file_success() { assert_eq!(response.status(), reqwest::StatusCode::OK); - // Content-Type 확인 let content_type = response .headers() .get("content-type") @@ -163,7 +156,6 @@ async fn get_file_success() { .unwrap(); assert_eq!(content_type, "text/html; charset=UTF-8"); - // 파일 내용 확인 let file_content = response.text().await.unwrap(); assert_eq!( file_content, @@ -176,11 +168,10 @@ async fn get_file_success() { async fn get_files_by_category_success() { let setup = setup_test(b"print('Hello, World!')", "hello.py").await; - // 파일 업로드 let upload_response = upload_file_request(&setup).await; assert_eq!(upload_response.status(), reqwest::StatusCode::CREATED); - // 카테고리별 파일 목록 조회 + // Retrieve file list by category let client = reqwest::Client::new(); let response = client .get(&format!( @@ -205,11 +196,10 @@ async fn get_files_by_category_success() { async fn delete_file_success() { let setup = setup_test(b"print('Hello, World!')", "delete-test.py").await; - // 먼저 파일 업로드 let upload_response = upload_file_request(&setup).await; assert_eq!(upload_response.status(), reqwest::StatusCode::CREATED); - // 파일이 존재하는지 확인 + // Check if file exists let expected_file_path = format!( "{}/{}/solution/{}", setup.temp_dir.path().to_str().unwrap(), @@ -218,7 +208,7 @@ async fn delete_file_success() { ); assert!(Path::new(&expected_file_path).exists()); - // 파일 삭제 + // Delete file let client = reqwest::Client::new(); let response = client .delete(&format!( @@ -240,7 +230,6 @@ async fn delete_file_success() { .unwrap() .contains("deleted successfully")); - // 파일이 실제로 삭제되었는지 확인 assert!(!Path::new(&expected_file_path).exists()); } @@ -249,7 +238,7 @@ async fn delete_file_success() { async fn delete_file_not_found() { let setup = setup_test(b"print('Hello, World!')", "not-found-test.py").await; - // 파일을 업로드하지 않고 바로 삭제 시도 + // Attempt to delete without uploading file first let client = reqwest::Client::new(); let response = client .delete(&format!( @@ -277,11 +266,10 @@ async fn delete_file_not_found() { async fn update_file_content_success() { let setup = setup_test(b"print(int(input()) - int(input()))", "update-test.py").await; - // 먼저 파일 업로드 let upload_response = upload_file_request(&setup).await; assert_eq!(upload_response.status(), reqwest::StatusCode::CREATED); - // 파일 수정 + // Modify file let client = reqwest::Client::new(); let update_data = serde_json::json!({ "content": "print(int(input()) + int(input()))" @@ -308,7 +296,7 @@ async fn update_file_content_success() { .unwrap() .contains("updated successfully")); - // 파일 내용이 실제로 업데이트되었는지 확인 + // Verify that file content was actually updated let expected_file_path = format!( "{}/{}/solution/{}", setup.temp_dir.path().to_str().unwrap(), @@ -328,7 +316,7 @@ async fn update_file_not_found() { ) .await; - // 파일을 업로드하지 않고 바로 수정 시도 + // Attempt to modify without uploading file first let client = reqwest::Client::new(); let update_data = serde_json::json!({ "content": "print('Updated content!')" @@ -361,11 +349,10 @@ async fn update_file_not_found() { async fn update_file_missing_content() { let setup = setup_test(b"print('Hello, World!')", "missing-content-test.py").await; - // 파일 업로드 let upload_response = upload_file_request(&setup).await; assert_eq!(upload_response.status(), reqwest::StatusCode::CREATED); - // content 필드 없이 수정 시도 + // Attempt to modify without content field let client = reqwest::Client::new(); let update_data = serde_json::json!({ "other_field": "some value" @@ -391,11 +378,10 @@ async fn update_file_missing_content() { async fn update_filename_success() { let setup = setup_test(b"print(int(input()) + int(input()))", "aplusb.py").await; - // 먼저 파일 업로드 let upload_response = upload_file_request(&setup).await; assert_eq!(upload_response.status(), reqwest::StatusCode::CREATED); - // 파일 수정 + // Modify file let client = reqwest::Client::new(); let new_filename = "aplusb-AC.py"; let update_data = serde_json::json!({ @@ -423,7 +409,7 @@ async fn update_filename_success() { .unwrap() .contains("updated successfully")); - // 파일 이름이 실제로 업데이트되었는지 확인 + // Verify that filename was actually updated let expected_file_path = format!( "{}/{}/solution/{}", setup.temp_dir.path().to_str().unwrap(), From 39c76785cfaae8afd61b0d950f35ebbb84aa1a6d Mon Sep 17 00:00:00 2001 From: reddevilmidzy Date: Sun, 21 Sep 2025 00:17:56 +0900 Subject: [PATCH 5/6] test: Change content-type to text/plain --- tests/file_manager/handlers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/file_manager/handlers.rs b/tests/file_manager/handlers.rs index a7e5cc6..f891293 100644 --- a/tests/file_manager/handlers.rs +++ b/tests/file_manager/handlers.rs @@ -154,7 +154,7 @@ async fn get_file_success() { .unwrap() .to_str() .unwrap(); - assert_eq!(content_type, "text/html; charset=UTF-8"); + assert_eq!(content_type, "text/plain; charset=UTF-8"); let file_content = response.text().await.unwrap(); assert_eq!( From 9276235cfaaecb73a11e23803b4829524d279486 Mon Sep 17 00:00:00 2001 From: reddevilmidzy Date: Sun, 21 Sep 2025 15:47:12 +0900 Subject: [PATCH 6/6] refactor: Remove rand crate --- Cargo.toml | 1 - src/file_manager/git.rs | 17 ++++++++--------- tests/file_manager/handlers.rs | 3 +-- 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d1b7a01..6af0ba6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,6 @@ tokio = { version = "1.45.1", features = ["full"] } uuid = { version = "1.17.0", features = ["v4"] } [dev-dependencies] -rand = "0.9" reqwest = { version = "0.12.19", features = ["json", "rustls-tls"] } rstest = "0.25.0" serial_test = "3.2" diff --git a/src/file_manager/git.rs b/src/file_manager/git.rs index 1faa0f5..a728ff8 100644 --- a/src/file_manager/git.rs +++ b/src/file_manager/git.rs @@ -199,7 +199,6 @@ struct FileInfo { #[cfg(test)] mod tests { use super::*; - use rand::Rng; use rstest::rstest; use std::path::Path; use tempfile::TempDir; @@ -233,7 +232,7 @@ mod tests { #[tokio::test] async fn can_init_git_repository() -> Result<(), std::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = rand::rng().random_range(0..=u32::MAX); + let problem_id = 0; let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); assert!(git_manager.git_init().is_ok()); assert!(Path::new(git_manager.get_upload_path().to_str().unwrap()).exists()); @@ -248,7 +247,7 @@ mod tests { #[tokio::test] async fn can_set_config() -> Result<(), std::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = rand::rng().random_range(0..=u32::MAX); + let problem_id = 0; let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); assert!(git_manager.git_init().is_ok()); let repo = git_manager.get_repository().unwrap(); @@ -265,7 +264,7 @@ mod tests { #[tokio::test] async fn can_create_default_file() -> Result<(), std::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = rand::rng().random_range(0..=u32::MAX); + let problem_id = 0; let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); assert!(git_manager.create_default_directories().await.is_ok()); assert!(Path::new( @@ -295,7 +294,7 @@ mod tests { #[tokio::test] async fn can_create_problem() -> Result<(), std::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = rand::rng().random_range(0..=u32::MAX); + let problem_id = 0; let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); assert!(git_manager.create_problem().await.is_ok()); assert!(Path::new(git_manager.get_upload_path().to_str().unwrap()).exists()); @@ -330,7 +329,7 @@ mod tests { #[tokio::test] async fn can_get_git_status() -> Result<(), tokio::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = rand::rng().random_range(0..=u32::MAX); + let problem_id = 0; let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); git_manager.git_init().unwrap(); @@ -376,7 +375,7 @@ mod tests { #[tokio::test] async fn can_git_add() -> Result<(), tokio::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = rand::rng().random_range(0..=u32::MAX); + let problem_id = 0; let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); git_manager.git_init().unwrap(); @@ -422,7 +421,7 @@ mod tests { #[tokio::test] async fn can_commit() -> Result<(), tokio::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = rand::rng().random_range(0..=u32::MAX); + let problem_id = 0; let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); git_manager.git_init().unwrap(); @@ -464,7 +463,7 @@ mod tests { #[tokio::test] async fn can_get_log() -> Result<(), tokio::io::Error> { let temp_dir = TempDir::new()?; - let problem_id = rand::rng().random_range(0..=u32::MAX); + let problem_id = 0; let git_manager = GitManager::new(problem_id, temp_dir.path().to_path_buf()); git_manager.git_init().unwrap(); git_manager.create_default_directories().await.unwrap(); diff --git a/tests/file_manager/handlers.rs b/tests/file_manager/handlers.rs index f891293..fca788c 100644 --- a/tests/file_manager/handlers.rs +++ b/tests/file_manager/handlers.rs @@ -1,4 +1,3 @@ -use rand::Rng; use std::path::Path; use tempfile::TempDir; use tokio::fs; @@ -13,7 +12,7 @@ struct TestSetup { } async fn setup_test(file_content: &[u8], filename: &str) -> TestSetup { - let problem_id = rand::rng().random_range(0..=u32::MAX); + let problem_id = 0; let temp_dir = TempDir::new().unwrap(); std::env::set_var("UPLOAD_DIR", temp_dir.path().to_str().unwrap());