From 6abc5bc1679b94364cc4da71f05390eb83667570 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=8F=E5=90=91=E5=A4=9C?= <46275354+fu050409@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:43:32 +0800 Subject: [PATCH] feat(problem): support delete problem and assets (#43) * feat(problem): support delete problem and assets * chore: fix test db port * chore: remove serde_json --- .changes/problem.md | 5 ++ Cargo.lock | 2 +- src/cors.rs | 2 +- src/main.rs | 8 ++- src/models/problem.rs | 6 ++ src/routes/index.rs | 10 ++- src/routes/problem.rs | 33 ++++++++- src/utils/problem.rs | 33 ++++++++- tests/1.in | 1 + tests/1.out | 1 + tests/account.rs | 4 +- tests/category.rs | 5 +- tests/organization.rs | 5 +- tests/problem.rs | 160 +++++++++++++++++++++++++++++++++++++++--- tests/utils.rs | 13 +++- 15 files changed, 264 insertions(+), 24 deletions(-) create mode 100644 .changes/problem.md create mode 100644 tests/1.in create mode 100644 tests/1.out diff --git a/.changes/problem.md b/.changes/problem.md new file mode 100644 index 0000000..9ff34c8 --- /dev/null +++ b/.changes/problem.md @@ -0,0 +1,5 @@ +--- +"algohub-server": patch:feat +--- + +Add endpoint to delete a problem by specified problem ID. diff --git a/Cargo.lock b/Cargo.lock index 189ccb6..e2f5872 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ dependencies = [ [[package]] name = "algohub-server" -version = "0.1.8" +version = "0.1.9" dependencies = [ "anyhow", "chrono", diff --git a/src/cors.rs b/src/cors.rs index 6937d1e..ed076d3 100644 --- a/src/cors.rs +++ b/src/cors.rs @@ -8,7 +8,7 @@ pub struct CORS; impl Fairing for CORS { fn info(&self) -> Info { Info { - name: "Add CORS headers to responses", + name: "CORS Policy", kind: Kind::Response, } } diff --git a/src/main.rs b/src/main.rs index 56f9892..06c210b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,12 @@ +use algohub_server::routes::index::init_db; use rocket::launch; #[launch] async fn rocket() -> _ { - algohub_server::rocket().await + algohub_server::rocket( + init_db("localhost:5177") + .await + .expect("Failed to initialize database, shutting down..."), + ) + .await } diff --git a/src/models/problem.rs b/src/models/problem.rs index 0b6d30b..0ef2bf4 100644 --- a/src/models/problem.rs +++ b/src/models/problem.rs @@ -163,3 +163,9 @@ impl From for UserProblem { } } } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServerTestCase { + pub input: String, + pub output: String, +} diff --git a/src/routes/index.rs b/src/routes/index.rs index e1b8622..1687b9f 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -9,6 +9,7 @@ use super::submission; use crate::{cors::CORS, routes::account}; use anyhow::Result; use rocket::fs::NamedFile; +use surrealdb::engine::remote::ws::Client; use surrealdb::{engine::remote::ws::Ws, opt::auth::Root, Surreal}; #[get("/")] @@ -21,10 +22,11 @@ async fn files(file: PathBuf) -> Option { NamedFile::open(Path::new("dist/").join(file)).await.ok() } -pub async fn rocket() -> rocket::Rocket { - let db = Surreal::new::("127.0.0.1:5176") +pub async fn init_db(db_addr: &str) -> Result> { + let db = Surreal::new::(db_addr) .await .expect("Failed to connect to database"); + db.use_ns("main") .use_db("acm") .await @@ -36,6 +38,10 @@ pub async fn rocket() -> rocket::Rocket { .await .expect("Failed to authenticate"); + Ok(db) +} + +pub async fn rocket(db: Surreal) -> rocket::Rocket { rocket::build() .attach(CORS) .mount("/", routes![index, files]) diff --git a/src/routes/problem.rs b/src/routes/problem.rs index da62197..47151b6 100644 --- a/src/routes/problem.rs +++ b/src/routes/problem.rs @@ -1,4 +1,4 @@ -use rocket::{serde::json::Json, State}; +use rocket::{serde::json::Json, tokio::fs::remove_file, State}; use serde::{Deserialize, Serialize}; use surrealdb::{engine::remote::ws::Client, Surreal}; @@ -7,7 +7,7 @@ use crate::{ account::Account, error::Error, problem::{CreateProblem, Problem, ProblemVisibility, UserProblem}, - response::Response, + response::{Empty, Response}, Credentials, OwnedCredentials, OwnedId, }, utils::{account, problem, session}, @@ -148,7 +148,34 @@ pub async fn list( })) } +#[delete("/delete/", data = "")] +pub async fn delete( + db: &State>, + id: &str, + auth: Json>, +) -> Result { + if !session::verify(db, auth.id, auth.token).await { + return Err(Error::Unauthorized(Json("Invalid credentials".into()))); + } + + for test_case in problem::get_test_cases_by_id(db, id).await? { + remove_file(test_case.input).await?; + remove_file(test_case.output).await?; + } + println!("Down"); + + problem::delete(db, id).await?.ok_or(Error::NotFound(Json( + "Problem with specified id not found".into(), + )))?; + + Ok(Json(Response { + success: true, + message: "Problem deleted successfully".to_string(), + data: None, + })) +} + pub fn routes() -> Vec { use rocket::routes; - routes![create, get, list] + routes![create, get, list, delete] } diff --git a/src/utils/problem.rs b/src/utils/problem.rs index 76d9796..6e0b46b 100644 --- a/src/utils/problem.rs +++ b/src/utils/problem.rs @@ -1,8 +1,8 @@ use anyhow::Result; use serde::Deserialize; -use surrealdb::{engine::remote::ws::Client, Surreal}; +use surrealdb::{engine::remote::ws::Client, sql::Thing, Surreal}; -use crate::models::problem::{CreateProblem, Problem}; +use crate::models::problem::{CreateProblem, Problem, ServerTestCase}; pub async fn create(db: &Surreal, problem: CreateProblem<'_>) -> Result> { Ok(db @@ -21,8 +21,20 @@ pub async fn update(db: &Surreal, problem: Problem) -> Result, id: &str) -> Result> { - Ok(db.delete(("problem", id)).await?) + Ok(db + .query(DELETE_ALL_ASSETS_QUERY) + .bind(("problem", Thing::from(("problem", id)))) + .await? + .take(0)?) } pub async fn get(db: &Surreal, id: &str) -> Result> @@ -68,3 +80,18 @@ where Ok(response.take(0)?) } + +const SELECT_TEST_CASES_QUERY: &str = r#" +IF $problem.exists() THEN + SELECT input.path AS input, output.path AS output FROM $problem.test_cases +ELSE + [] +END; +"#; +pub async fn get_test_cases_by_id(db: &Surreal, id: &str) -> Result> { + Ok(db + .query(SELECT_TEST_CASES_QUERY) + .bind(("problem", Thing::from(("problem", id)))) + .await? + .take(0)?) +} diff --git a/tests/1.in b/tests/1.in new file mode 100644 index 0000000..1c6ae71 --- /dev/null +++ b/tests/1.in @@ -0,0 +1 @@ +1 2 \ No newline at end of file diff --git a/tests/1.out b/tests/1.out new file mode 100644 index 0000000..e440e5c --- /dev/null +++ b/tests/1.out @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/tests/account.rs b/tests/account.rs index 3f095a5..b2c79d6 100644 --- a/tests/account.rs +++ b/tests/account.rs @@ -10,11 +10,11 @@ use algohub_server::models::{ }; use anyhow::Result; use rocket::{http::ContentType, local::asynchronous::Client}; -use utils::Upload; +use utils::{rocket, Upload}; #[rocket::async_test] async fn test_register() -> Result<()> { - let rocket = algohub_server::rocket().await; + let rocket = rocket().await; let client = Client::tracked(rocket).await?; diff --git a/tests/category.rs b/tests/category.rs index 6497d4f..2ba7c4b 100644 --- a/tests/category.rs +++ b/tests/category.rs @@ -1,3 +1,5 @@ +mod utils; + use algohub_server::models::{ account::Register, category::{Category, CategoryData, CreateCategory, ListCategories}, @@ -6,10 +8,11 @@ use algohub_server::models::{ }; use anyhow::Result; use rocket::local::asynchronous::Client; +use utils::rocket; #[rocket::async_test] async fn test_category() -> Result<()> { - let rocket = algohub_server::rocket().await; + let rocket = rocket().await; let client = Client::tracked(rocket).await?; println!("Testing category..."); diff --git a/tests/organization.rs b/tests/organization.rs index 85a2bac..1bfd137 100644 --- a/tests/organization.rs +++ b/tests/organization.rs @@ -1,3 +1,5 @@ +mod utils; + use std::path::Path; use algohub_server::models::{ @@ -8,10 +10,11 @@ use algohub_server::models::{ }; use anyhow::Result; use rocket::local::asynchronous::Client; +use utils::rocket; #[rocket::async_test] async fn test_organization() -> Result<()> { - let rocket = algohub_server::rocket().await; + let rocket = rocket().await; let client = Client::tracked(rocket).await?; diff --git a/tests/problem.rs b/tests/problem.rs index 95d38e9..a591c15 100644 --- a/tests/problem.rs +++ b/tests/problem.rs @@ -1,22 +1,33 @@ +pub mod utils; + +use std::{fs::File, path::Path}; + use algohub_server::{ models::{ account::Register, - problem::{CreateProblem, ProblemVisibility, UserProblem}, + asset::UserContent, + problem::{CreateProblem, ProblemVisibility, UserProblem, UserTestCase}, response::{Empty, Response}, - OwnedCredentials, Token, UserRecordId, + Credentials, OwnedCredentials, Token, UserRecordId, + }, + routes::{ + index::init_db, + problem::{ListProblem, ProblemResponse}, }, - routes::problem::{ListProblem, ProblemResponse}, }; use anyhow::Result; -use rocket::local::asynchronous::Client; +use rocket::{http::ContentType, local::asynchronous::Client}; +use utils::Upload; #[rocket::async_test] async fn test_problem() -> Result<()> { - let rocket = algohub_server::rocket().await; + let db = init_db(utils::TEST_DB_ADDR) + .await + .expect("Failed to initialize database, shutting down"); + let rocket = algohub_server::rocket(db.clone()).await; let client = Client::tracked(rocket).await?; - println!("Testing register..."); let response = client .post("/account/create") .json(&Register { @@ -101,9 +112,9 @@ async fn test_problem() -> Result<()> { .into_json::>>() .await .unwrap(); - let data = data.unwrap(); + let problems = data.unwrap(); assert!(success); - assert_eq!(data.len(), 10); + assert_eq!(problems.len(), 10); let response = client .post("/problem/list") @@ -130,6 +141,139 @@ async fn test_problem() -> Result<()> { assert!(success); assert_eq!(data.len(), 3); + for problem in problems { + let response = client + .delete(format!("/problem/delete/{}", problem.id)) + .json(&Credentials { + id: &id, + token: &token, + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + + let Response { + success, + message: _, + data: _, + } = response.into_json::>().await.unwrap(); + assert!(success); + } + + let input = client + .put("/asset/upload") + .header(ContentType::new("multipart", "form-data").with_params(("boundary", "boundary"))) + .body(Upload { + auth: Credentials { + id: &id, + token: &token, + }, + owner_id: &id, + file: File::open("tests/1.in")?, + }) + .dispatch() + .await; + let Response { + success, + message: _, + data, + } = input.into_json::>().await.unwrap(); + assert!(success); + let input = data.unwrap(); + + let output = client + .put("/asset/upload") + .header(ContentType::new("multipart", "form-data").with_params(("boundary", "boundary"))) + .body(Upload { + auth: Credentials { + id: &id, + token: &token, + }, + owner_id: &id, + file: File::open("tests/1.out")?, + }) + .dispatch() + .await; + let Response { + success, + message: _, + data, + } = output.into_json::>().await.unwrap(); + assert!(success); + let output = data.unwrap(); + + let response = client + .post("/problem/create") + .json(&CreateProblem { + id: &id, + token: &token, + title: "Test Problem", + description: "Test Description".to_string(), + input: Some("Test Input".to_string()), + output: Some("Test Output".to_string()), + samples: vec![], + hint: None, + owner: UserRecordId { + tb: "account".to_string(), + id: id.clone(), + }, + time_limit: 1000, + memory_limit: 128, + test_cases: vec![UserTestCase { + input: &input.id, + output: &output.id, + }], + categories: vec![], + tags: vec![], + visibility: ProblemVisibility::Public, + }) + .dispatch() + .await; + assert_eq!(response.status().code, 200); + + let Response { + success, + message: _, + data, + } = response.into_json().await.unwrap(); + let data: ProblemResponse = data.unwrap(); + assert!(success); + + let test_cases = algohub_server::utils::problem::get_test_cases_by_id(&db, &data.id).await?; + for test_case in &test_cases { + assert!(Path::new(&test_case.input).exists()); + assert!(Path::new(&test_case.input).exists()); + } + + let response = client + .delete(format!("/problem/delete/{}", data.id)) + .json(&Credentials { + id: &id, + token: &token, + }) + .dispatch() + .await; + + assert_eq!(response.status().code, 200); + + let Response { + success, + message: _, + data: _, + } = response.into_json::>().await.unwrap(); + assert!(success); + + for test_case in test_cases { + assert!(!Path::new(&test_case.input).exists()); + assert!(!Path::new(&test_case.input).exists()); + } + assert!( + algohub_server::utils::problem::get_test_cases_by_id(&db, &data.id) + .await? + .is_empty() + ); + client .post(format!("/account/delete/{}", id)) .json(&Token { token: &token }) diff --git a/tests/utils.rs b/tests/utils.rs index fa15409..dd03123 100644 --- a/tests/utils.rs +++ b/tests/utils.rs @@ -1,6 +1,6 @@ use std::{fs::File, io::Read}; -use algohub_server::models::Credentials; +use algohub_server::{models::Credentials, routes::index::init_db}; pub struct Upload<'a> { pub auth: Credentials<'a>, @@ -56,3 +56,14 @@ impl AsRef<[u8]> for Upload<'_> { body.leak() } } + +pub const TEST_DB_ADDR: &str = "localhost:5176"; + +pub async fn rocket() -> rocket::Rocket { + algohub_server::rocket( + init_db(TEST_DB_ADDR) + .await + .expect("Failed to initialize database, shutting down"), + ) + .await +}