diff --git a/Cargo.lock b/Cargo.lock index fae69a8..cefd85c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,7 +86,7 @@ dependencies = [ "axum-extra", "bytes", "cfg-if", - "http", + "http 1.1.0", "indexmap 2.6.0", "schemars", "serde", @@ -277,6 +277,16 @@ dependencies = [ "term", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -333,7 +343,7 @@ dependencies = [ "fnv", "futures-timer", "futures-util", - "http", + "http 1.1.0", "indexmap 2.6.0", "mime", "multer", @@ -445,6 +455,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto-future" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" + [[package]] name = "autocfg" version = "1.4.0" @@ -463,7 +479,7 @@ dependencies = [ "base64 0.22.1", "bytes", "futures-util", - "http", + "http 1.1.0", "http-body", "http-body-util", "hyper", @@ -498,7 +514,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", + "http 1.1.0", "http-body", "http-body-util", "mime", @@ -521,7 +537,7 @@ dependencies = [ "bytes", "cookie", "futures-util", - "http", + "http 1.1.0", "http-body", "http-body-util", "mime", @@ -550,6 +566,40 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "605877320e5ef98ba6bf000eeae03065557b0631d645c08ad6e84340d388948f" +[[package]] +name = "axum-test" +version = "16.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "017cbca2776229a7100ebee44e065fcf5baccea6fc4cb9e5bea8328d83863a03" +dependencies = [ + "anyhow", + "assert-json-diff", + "auto-future", + "axum", + "base64 0.22.1", + "bytes", + "bytesize", + "cookie", + "futures-util", + "http 1.1.0", + "http-body-util", + "hyper", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tokio-tungstenite 0.24.0", + "tower", + "url", + "uuid", +] + [[package]] name = "backtrace" version = "0.3.71" @@ -796,6 +846,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bytesize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" + [[package]] name = "cached" version = "0.53.1" @@ -1326,6 +1382,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "diffy" version = "0.4.0" @@ -1826,7 +1888,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.1.0", "indexmap 2.6.0", "slab", "tokio", @@ -1978,6 +2040,17 @@ dependencies = [ "syn 2.0.79", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.1.0" @@ -1996,7 +2069,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.1.0", ] [[package]] @@ -2007,7 +2080,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http", + "http 1.1.0", "http-body", "pin-project-lite", ] @@ -2032,15 +2105,15 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" dependencies = [ "bytes", "futures-channel", "futures-util", "h2", - "http", + "http 1.1.0", "http-body", "httparse", "httpdate", @@ -2058,7 +2131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", - "http", + "http 1.1.0", "hyper", "hyper-util", "rustls", @@ -2094,7 +2167,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", + "http 1.1.0", "http-body", "hyper", "pin-project-lite", @@ -2443,13 +2516,16 @@ dependencies = [ "axum", "axum-extra", "axum-swagger-ui", + "axum-test", + "base64 0.22.1", "bytes", "cached", "chrono", "dotenvy", + "flate2", "futures", "hashlink", - "http", + "http 1.1.0", "itertools 0.13.0", "jwt-simple", "reqwest", @@ -2463,6 +2539,7 @@ dependencies = [ "tower-http", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -2598,7 +2675,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-util", - "http", + "http 1.1.0", "httparse", "memchr", "mime", @@ -3180,6 +3257,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "primeorder" version = "0.13.6" @@ -3498,7 +3585,7 @@ dependencies = [ "futures-core", "futures-util", "h2", - "http", + "http 1.1.0", "http-body", "http-body-util", "hyper", @@ -3537,6 +3624,16 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "reserve-port" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9838134a2bfaa8e1f40738fcc972ac799de6e0e06b5157acb95fc2b05a0ea283" +dependencies = [ + "lazy_static", + "thiserror", +] + [[package]] name = "revision" version = "0.10.0" @@ -3698,6 +3795,22 @@ dependencies = [ "trim-in-place", ] +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b748410c0afdef2ebbe3685a6a862e2ee937127cdaae623336a459451c8d57" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http 0.2.12", + "mime", + "mime_guess", + "rand", + "thiserror", +] + [[package]] name = "rust-stemmers" version = "1.2.0" @@ -4735,9 +4848,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ "backtrace", "bytes", @@ -4868,7 +4981,7 @@ dependencies = [ "bitflags", "bytes", "futures-core", - "http", + "http 1.1.0", "http-body", "pin-project-lite", "tokio", @@ -4994,7 +5107,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.1.0", "httparse", "log", "rand", @@ -5015,7 +5128,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.1.0", "httparse", "log", "rand", @@ -5149,9 +5262,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", "serde", @@ -5521,6 +5634,12 @@ dependencies = [ "tap", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 448e633..9a7b7b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,3 +47,10 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } [patch.crates-io] serde = { git = "https://github.com/frederik-uni/serde" } + +[dev-dependencies] +axum-test = { version = "16.4.0", features = ["ws"] } +# to resolve conflict with axum-test and surrealdb +base64 = "0.22.1" +flate2 = "1.0" +uuid = "1.11" diff --git a/src/error.rs b/src/error.rs index 6e5c40f..7bc67f3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -62,6 +62,9 @@ pub enum AppError { #[error("Input string exceeds maximum length")] StringTooLong, + + #[error("Std IO error: {0}")] + StdIO(#[from] std::io::Error), } #[derive(Serialize)] @@ -85,6 +88,7 @@ impl IntoResponse for AppError { | AppError::TaskJoin(_) | AppError::ActivityStreamClosed | AppError::SurrealDbSerialization(_) + | AppError::StdIO(_) | AppError::SephomoreError(_) => StatusCode::INTERNAL_SERVER_ERROR, AppError::MissingTokenCookie | AppError::JwtVerification @@ -92,7 +96,6 @@ impl IntoResponse for AppError { AppError::MissingLayerJson | AppError::StringTooLong => { StatusCode::UNPROCESSABLE_ENTITY } - AppError::MissingInfluence | AppError::MissingUser(_) | Self::NonExistingMap(_) => { StatusCode::NOT_FOUND } diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index e7acb97..b993361 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -11,7 +11,7 @@ use futures::try_join; use http::HeaderMap; use reqwest::header::SET_COOKIE; use schemars::JsonSchema; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::{error::AppError, AppState}; @@ -28,13 +28,19 @@ pub struct AuthQuery { code: String, } -#[derive(Deserialize, JsonSchema)] +#[derive(Serialize, Deserialize, JsonSchema)] pub struct AdminLogin { password: String, /// Id of their osu account. This is so that they can act as their own account id: u32, } +impl AdminLogin { + pub fn new(password: String, id: u32) -> Self { + Self { password, id } + } +} + pub async fn osu_oauth2_redirect( Query(query_parameters): Query, State(state): State>, diff --git a/src/lib.rs b/src/lib.rs index 383458f..9170a8b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,10 +36,11 @@ pub struct AppState { } impl AppState { - pub async fn new( - request: Arc, - credentials_grant_client: Arc, - ) -> AppState { + pub async fn new(request: Arc) -> Arc { + let credentials_grant_client = CredentialsGrantClient::new(request.clone()) + .await + .expect("Failed to initialize credentials grant client"); + let db = Arc::new( DatabaseClient::new() .await @@ -59,7 +60,7 @@ impl AppState { // TODO: better handle errors .expect("failed to initialize activity tracker"); - AppState { + Arc::new(AppState { db, request: request.clone(), jwt: JwtUtil::new_jwt(), @@ -69,7 +70,7 @@ impl AppState { user_leaderboard_cache: LeaderboardCache::new(300), beatmap_leaderboard_cache: LeaderboardCache::new(300), graph_cache: GraphCache::new(600), - } + }) } } diff --git a/src/main.rs b/src/main.rs index 0ae16dd..e6376e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,10 +7,7 @@ use axum::{ Extension, Json, }; use axum_swagger_ui::swagger_ui; -use mapper_influences_backend_rs::{ - osu_api::{credentials_grant::CredentialsGrantClient, request::OsuApiRequestClient}, - routes, AppState, -}; +use mapper_influences_backend_rs::{osu_api::request::OsuApiRequestClient, routes, AppState}; use tower_http::{compression::CompressionLayer, cors::CorsLayer, trace::TraceLayer}; use tracing::info; use tracing_subscriber::fmt::format::FmtSpan; @@ -26,10 +23,7 @@ async fn main() { // initializing client wrappers and state let request = Arc::new(OsuApiRequestClient::new(10)); - let client_credential_client = CredentialsGrantClient::new(request.clone()) - .await - .expect("Failed to initialize credentials grant client"); - let state = Arc::new(AppState::new(request, client_credential_client).await); + let state = AppState::new(request).await; aide::gen::on_error(|error| { println!("{error}"); diff --git a/src/osu_api/cached_requester.rs b/src/osu_api/cached_requester.rs index fec05c6..1aab3c4 100644 --- a/src/osu_api/cached_requester.rs +++ b/src/osu_api/cached_requester.rs @@ -43,7 +43,6 @@ impl CachedRequester { let mut cache = self.cache.lock().map_err(|_| AppError::Mutex)?; cache.get_multiple(ids) }; - // Request the missing items let misses_requested = self .client diff --git a/src/osu_api/mod.rs b/src/osu_api/mod.rs index 72f2dcb..079a6aa 100644 --- a/src/osu_api/mod.rs +++ b/src/osu_api/mod.rs @@ -18,11 +18,9 @@ static REDIRECT_URI: LazyLock = LazyLock::new(|| { std::env::var("REDIRECT_URI").expect("Missing REDIRECT_URI environment variable") }); -/// Also has `refresh_token` but we don't need it -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Default)] pub struct OsuAuthToken { pub access_token: String, - pub token_type: String, pub expires_in: u32, } diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..f7e317b --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,102 @@ +use std::sync::Arc; + +use axum::{ + middleware, + routing::{any, delete, get, patch, post}, + Router, +}; +use axum_test::TestServer; +use mapper_influences_backend_rs::{handlers, osu_api::request::OsuApiRequestClient, AppState}; +use osu_test_client::OsuApiTestClient; + +pub mod osu_test_client; + +/// Redefining routes because aide and axum_test is not compatible +pub fn test_routes(state: Arc) -> Router> { + Router::new() + .route("/search/map", get(handlers::osu_search::osu_beatmap_search)) + .route( + "/search/user/:query", + get(handlers::osu_search::osu_user_search), + ) + .route( + "/influence/:influenced_to", + post(handlers::influence::add_influence), + ) + .route( + "/influence/influences/:user_id", + get(handlers::influence::get_user_influences), + ) + .route( + "/influence/mentions/:user_id", + get(handlers::influence::get_user_mentions), + ) + .route( + "/influence/:influenced_to", + delete(handlers::influence::delete_influence), + ) + .route( + "/influence/:influenced_to/map/:beatmap_id", + patch(handlers::influence::add_influence_beatmap), + ) + .route( + "/influence/:influenced_to/map/:beatmap_id", + delete(handlers::influence::remove_influence_beatmap), + ) + .route( + "/influence/:influenced_to/description", + patch(handlers::influence::update_influence_description), + ) + .route( + "/influence/:influenced_to/type/:type_id", + patch(handlers::influence::update_influence_type), + ) + .route("/users/me", get(handlers::user::get_me)) + .route("/users/:user_id", get(handlers::user::get_user)) + .route("/users/bio", patch(handlers::user::update_user_bio)) + .route( + "/users/map/:beatmap_id", + patch(handlers::user::add_user_beatmap), + ) + .route( + "/users/map/:beatmap_id", + delete(handlers::user::delete_user_beatmap), + ) + .route( + "/users/influence-order", + post(handlers::user::set_influence_order), + ) + .layer(middleware::from_fn_with_state( + state, + handlers::auth::check_jwt_token, + )) + .route("/activity", get(handlers::activity::get_latest_activities)) + .route("/ws", any(handlers::activity::ws_handler)) + .route( + "/oauth/osu-redirect", + get(handlers::auth::osu_oauth2_redirect), + ) + .route("/oauth/logout", get(handlers::auth::logout)) + .route("/oauth/admin", post(handlers::auth::admin_login)) + .route( + "/leaderboard/user", + get(handlers::leaderboard::get_user_leaderboard), + ) + .route( + "/leaderboard/beatmap", + get(handlers::leaderboard::get_beatmap_leaderboard), + ) + .route("/graph", get(handlers::graph_vizualizer::get_graph_data)) +} + +pub async fn init_test_env(label: &str) -> (TestServer, Arc) { + dotenvy::dotenv().ok(); + + let working_request = Arc::new(OsuApiRequestClient::new(10)); + let test_request_client = OsuApiTestClient::new(working_request.clone(), label); + let state = AppState::new(test_request_client.clone()).await; + + let routes = test_routes(state.clone()).with_state(state); + let test_server = TestServer::new(routes).expect("failed to initialize test server"); + (test_server, test_request_client) +} diff --git a/tests/common/osu_test_client.rs b/tests/common/osu_test_client.rs new file mode 100644 index 0000000..ece1ad7 --- /dev/null +++ b/tests/common/osu_test_client.rs @@ -0,0 +1,179 @@ +use std::{ + collections::HashMap, + fs::File, + io::{BufReader, BufWriter, Read, Write}, + sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard}, +}; + +use axum::async_trait; +use bytes::Bytes; +use flate2::{read::GzDecoder, write::GzEncoder, Compression}; +use futures::future::try_join_all; +use itertools::Itertools; +use mapper_influences_backend_rs::{ + error::AppError, + osu_api::{ + request::{OsuApiRequestClient, Requester}, + AuthRequest, OsuAuthToken, + }, +}; +use serde_json::Value; + +const OSU_CACHE_BASE_PATH: &str = "tests/data"; + +#[derive(Debug)] +pub enum ClientMod { + Replay, + Record, +} + +pub struct OsuApiTestClient { + pub working_client: Arc, + pub request_cache: RwLock>, + pub path: String, + pub client_mod: ClientMod, +} + +fn read_osu_request_cache(file_path: &str) -> Option> { + let file = File::open(file_path).ok()?; + let mut decoder = GzDecoder::new(BufReader::new(file)); + let mut decompressed_data = Vec::new(); + decoder.read_to_end(&mut decompressed_data).ok()?; + + let deserialized: HashMap> = serde_json::from_slice(&decompressed_data).ok()?; + + Some( + deserialized + .into_iter() + .map(|(k, v)| (k, Bytes::from(v))) + .collect(), + ) +} + +fn save_osu_request_cache(file_path: &str, cache: &HashMap) -> std::io::Result<()> { + let file = File::create(file_path)?; + let mut encoder = GzEncoder::new(BufWriter::new(file), Compression::default()); + + let serializable_cache: HashMap> = + cache.iter().map(|(k, v)| (k.clone(), v.to_vec())).collect(); + + let serialized = serde_json::to_vec(&serializable_cache)?; + encoder.write_all(&serialized)?; + encoder.finish()?; + Ok(()) +} +impl OsuApiTestClient { + pub fn new(working_client: Arc, label: &str) -> Arc { + let path = format!("{}/{}", OSU_CACHE_BASE_PATH, label); + let cache = read_osu_request_cache(&path); + let client_mod = if cache.is_none() { + ClientMod::Record + } else { + ClientMod::Replay + }; + + let cache = cache.unwrap_or_default(); + let request_cache = RwLock::new(cache); + + Arc::new(OsuApiTestClient { + working_client, + path, + client_mod, + request_cache, + }) + } + + fn read_cache_lock(&self) -> Result>, AppError> { + self.request_cache.read().map_err(|_| AppError::RwLock) + } + fn write_cache_lock(&self) -> Result>, AppError> { + self.request_cache.write().map_err(|_| AppError::RwLock) + } + + pub fn save_cache(&self) -> Result<(), AppError> { + if let ClientMod::Record = self.client_mod { + let cache = self + .read_cache_lock() + .map_err(|_| AppError::RwLock)? + .clone(); + save_osu_request_cache(&self.path, &cache)?; + } + Ok(()) + } +} + +#[async_trait] +impl Requester for OsuApiTestClient { + async fn get_request(&self, url: &str, token: &str) -> Result { + match &self.client_mod { + ClientMod::Replay => { + let read_cache_lock = self.read_cache_lock()?; + let bytes = read_cache_lock.get(url).unwrap_or_else(|| { + panic!( + "Missing cache entry in {} \ + Please delete the cache file to record requests again", + self.path + ) + }); + Ok(bytes.clone()) + } + + ClientMod::Record => { + let bytes = self.working_client.get_request(url, token).await?; + self.write_cache_lock()? + .insert(url.to_string(), bytes.clone()); + Ok(bytes) + } + } + } + async fn post_request(&self, _url: &str, _body: AuthRequest) -> Result { + unreachable!() + } + async fn get_client_credentials_token(&self) -> Result { + match &self.client_mod { + ClientMod::Replay => Ok(OsuAuthToken::default()), + ClientMod::Record => Ok(self.working_client.get_client_credentials_token().await?), + } + } + + /// reimplementing the same function but with sorting to keep the cache keys in order + /// we don't need to sort in production so it's better to add it here + async fn request_multiple( + self: Arc, + base_url: &str, + keys: &[u32], + access_token: &str, + ) -> Result, AppError> { + let mut handlers = Vec::new(); + + // this is where we add sorting + for chunk_ids in &keys.iter().sorted().chunks(50) { + let url = format!( + "{}?{}", + base_url, + chunk_ids + .map(|id| format!("ids[]={}", id)) + .collect::>() + .join("&") + ); + let access_token_string = access_token.to_string(); + let self_clone = Arc::clone(&self); + + let handler = tokio::spawn(async move { + let response: Result, AppError> = self_clone + .deserialize_without_outer(url, access_token_string) + .await; + response + }); + handlers.push(handler); + } + + try_join_all(handlers) + .await? + .into_iter() + .try_fold(vec![], |mut acc, result| { + acc.extend(result?); + Ok(acc) + }) + } +} diff --git a/tests/data/BeatmapLeaderboard b/tests/data/BeatmapLeaderboard new file mode 100644 index 0000000..0768a77 Binary files /dev/null and b/tests/data/BeatmapLeaderboard differ diff --git a/tests/data/UserBeatmapAdd b/tests/data/UserBeatmapAdd new file mode 100644 index 0000000..cd1343a Binary files /dev/null and b/tests/data/UserBeatmapAdd differ diff --git a/tests/leaderboard.rs b/tests/leaderboard.rs new file mode 100644 index 0000000..d7df1ce --- /dev/null +++ b/tests/leaderboard.rs @@ -0,0 +1,11 @@ +use common::init_test_env; + +mod common; + +#[tokio::test] +async fn test_beatmap_leaderboard() { + const TEST_LABEL: &str = "BeatmapLeaderboard"; + let (test_server, test_requester) = init_test_env(TEST_LABEL).await; + let _response = test_server.get("/leaderboard/beatmap").await; + test_requester.save_cache().expect("failed to save cache"); +} diff --git a/tests/user.rs b/tests/user.rs new file mode 100644 index 0000000..fc8a23a --- /dev/null +++ b/tests/user.rs @@ -0,0 +1,27 @@ +use common::init_test_env; +use http::header::COOKIE; +use mapper_influences_backend_rs::handlers::auth::AdminLogin; + +mod common; + +#[tokio::test] +async fn test_beatmap_leaderboard() { + const TEST_LABEL: &str = "UserBeatmapAdd"; + let (test_server, test_requester) = init_test_env(TEST_LABEL).await; + + let oauth_body = AdminLogin::new(std::env::var("ADMIN_PASSWORD").unwrap(), 2); + let jwt = test_server + .post("/oauth/admin") + .json(&oauth_body) + .await + .text(); + + let result = test_server + .patch("/users/map/4776938") + .add_header(COOKIE, format!("user_token={}", jwt)) + .await + .text(); + + dbg!(result); + test_requester.save_cache().expect("failed to save cache"); +}