From ade8c162cdd3308ae30b109e8b4c63a493f9c9af Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Fri, 24 Nov 2023 11:42:06 -0800 Subject: [PATCH 1/3] Staging fixes (#766) * fixes bugs * fixed more things * fixes version creation * small change * removed modpack --- ...1700_adds_missing_loader_field_loaders.sql | 45 +++++++++++++++++++ src/routes/v2/projects.rs | 10 ++--- src/routes/v2/version_creation.rs | 35 +++++++++++---- 3 files changed, 75 insertions(+), 15 deletions(-) create mode 100644 migrations/20231122111700_adds_missing_loader_field_loaders.sql diff --git a/migrations/20231122111700_adds_missing_loader_field_loaders.sql b/migrations/20231122111700_adds_missing_loader_field_loaders.sql new file mode 100644 index 00000000..8747a2cb --- /dev/null +++ b/migrations/20231122111700_adds_missing_loader_field_loaders.sql @@ -0,0 +1,45 @@ + +-- Adds missing fields to loader_fields_loaders +INSERT INTO loader_fields_loaders (loader_id, loader_field_id) +SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field = 'game_versions' +AND l.loader = ANY( ARRAY['forge', 'fabric', 'quilt', 'modloader','rift','liteloader', 'neoforge']) +ON CONFLICT (loader_id, loader_field_id) DO NOTHING; + +-- Fixes mrpack variants being added to the wrong enum +-- Luckily, mrpack variants are the only ones set to 2 without metadata +UPDATE loader_field_enum_values SET enum_id = 3 WHERE enum_id = 2 AND metadata IS NULL; + +-- Because it was mislabeled, version_fields for mrpack_loaders were set to null. +-- 1) Update version_fields corresponding to mrpack_loaders to the correct enum_value +UPDATE version_fields vf +SET enum_value = subquery.lfev_id +FROM ( + SELECT vf.version_id, vf.field_id, lfev.id AS lfev_id + FROM version_fields vf + LEFT JOIN versions v ON v.id = vf.version_id + LEFT JOIN loaders_versions lv ON v.id = lv.version_id + LEFT JOIN loaders l ON l.id = lv.loader_id + LEFT JOIN loader_fields lf ON lf.id = vf.field_id + LEFT JOIN loader_field_enum_values lfev ON lfev.value = l.loader AND lf.enum_type = lfev.enum_id + WHERE lf.field = 'mrpack_loaders' AND vf.enum_value IS NULL +) AS subquery +WHERE vf.version_id = subquery.version_id AND vf.field_id = subquery.field_id; + +-- 2) Set those versions to mrpack as their version +INSERT INTO loaders_versions (version_id, loader_id) +SELECT DISTINCT vf.version_id, l.id +FROM version_fields vf +LEFT JOIN loader_fields lf ON lf.id = vf.field_id +CROSS JOIN loaders l +WHERE lf.field = 'mrpack_loaders' +AND l.loader = 'mrpack' +ON CONFLICT DO NOTHING; + +-- 3) Delete the old versions that had mrpack added to them +DELETE FROM loaders_versions lv +WHERE lv.loader_id != (SELECT id FROM loaders WHERE loader = 'mrpack') +AND lv.version_id IN ( + SELECT version_id + FROM loaders_versions + WHERE loader_id = (SELECT id FROM loaders WHERE loader = 'mrpack') +); diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index 8755b95c..c1a75db7 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -117,14 +117,10 @@ pub async fn random_projects_get( let response = v3::projects::random_projects_get(web::Query(count), pool.clone(), redis.clone()).await?; // Convert response to V2 format - match v2_reroute::extract_ok_json::(response).await { + match v2_reroute::extract_ok_json::>(response).await { Ok(project) => { - let version_item = match project.versions.first() { - Some(vid) => version_item::Version::get((*vid).into(), &**pool, &redis).await?, - None => None, - }; - let project = LegacyProject::from(project, version_item); - Ok(HttpResponse::Ok().json(project)) + let legacy_projects = LegacyProject::from_many(project, &**pool, &redis).await?; + Ok(HttpResponse::Ok().json(legacy_projects)) } Err(response) => Ok(response), } diff --git a/src/routes/v2/version_creation.rs b/src/routes/v2/version_creation.rs index 3e1de740..4bfa9613 100644 --- a/src/routes/v2/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -100,14 +100,33 @@ pub async fn version_create( fields.insert("client_side".to_string(), json!("required")); fields.insert("server_side".to_string(), json!("optional")); - // TODO: Some kind of handling here to ensure project type is fine. - // We expect the version uploaded to be of loader type modpack, but there might not be a way to check here for that. - // After all, theoretically, they could be creating a genuine 'fabric' mod, and modpack no longer carries information on whether its a mod or modpack, - // as those are out to the versions. + // Handle project type via file extension prediction + let mut project_type = None; + for file_part in &legacy_create.file_parts { + if let Some(ext) = file_part.split('.').last() { + match ext { + "mrpack" => { + project_type = Some("modpack"); + break; + } + // No other type matters + _ => {} + } + break; + } + } - // Ideally this would, if the project 'should' be a modpack: - // - change the loaders to mrpack only - // - add loader fields to the project for the corresponding loaders + // Modpacks now use the "mrpack" loader, and loaders are converted to loader fields. + // Setting of 'project_type' directly is removed, it's loader-based now. + if project_type == Some("modpack") { + fields.insert("mrpack_loaders".to_string(), json!(legacy_create.loaders)); + } + + let loaders = if project_type == Some("modpack") { + vec![Loader("mrpack".to_string())] + } else { + legacy_create.loaders + }; Ok(v3::version_creation::InitialVersionData { project_id: legacy_create.project_id, @@ -117,7 +136,7 @@ pub async fn version_create( version_body: legacy_create.version_body, dependencies: legacy_create.dependencies, release_channel: legacy_create.release_channel, - loaders: legacy_create.loaders, + loaders, featured: legacy_create.featured, primary_file: legacy_create.primary_file, status: legacy_create.status, From 172b93d07f095b69958ad2bef99c44a01544bbb2 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Sat, 25 Nov 2023 13:42:39 -0800 Subject: [PATCH 2/3] Tests v2 recreate (#760) * added common project information; setup for v2 test change * all tests now use with_test_environment * progress, failing * finished re-adding tests * prepare * cargo sqlx prepare -- --tests * fmt; clippy; prepare * sqlx prepare * adds version_create fix and corresponding test * merge fixes; rev * fmt, clippy, prepare * test cargo sqlx prepare --- ...3e3a7ad0f9a1e5a457770608bc41dbb83f2dd.json | 17 + Cargo.lock | 22 + Cargo.toml | 2 +- src/models/v2/projects.rs | 6 +- src/routes/v2/version_creation.rs | 2 +- src/routes/v3/project_creation.rs | 1 - tests/analytics.rs | 406 +-- tests/common/api_common/generic.rs | 136 + tests/common/api_common/mod.rs | 262 ++ tests/common/api_common/models.rs | 141 + tests/common/api_v2/mod.rs | 26 +- tests/common/api_v2/project.rs | 131 +- tests/common/api_v2/request_data.rs | 25 +- tests/common/api_v2/tags.rs | 50 +- tests/common/api_v2/team.rs | 65 +- tests/common/api_v2/version.rs | 270 +- tests/common/api_v3/mod.rs | 26 +- tests/common/api_v3/oauth.rs | 2 +- tests/common/api_v3/oauth_clients.rs | 2 +- tests/common/api_v3/organization.rs | 2 + tests/common/api_v3/project.rs | 71 +- tests/common/api_v3/request_data.rs | 20 +- tests/common/api_v3/tags.rs | 29 +- tests/common/api_v3/team.rs | 64 +- tests/common/api_v3/version.rs | 263 +- tests/common/asserts.rs | 10 + tests/common/database.rs | 21 +- tests/common/dummy_data.rs | 100 +- tests/common/environment.rs | 90 +- tests/common/mod.rs | 1 + tests/common/permissions.rs | 237 +- tests/common/scopes.rs | 13 +- tests/games.rs | 27 +- tests/loader_fields.rs | 581 ++-- tests/notifications.rs | 28 +- tests/oauth.rs | 69 +- tests/oauth_clients.rs | 41 +- tests/organizations.rs | 1104 +++---- tests/pats.rs | 414 ++- tests/project.rs | 1707 +++++------ tests/scopes.rs | 2528 +++++++++-------- tests/search.rs | 538 ++-- tests/tags.rs | 66 +- tests/teams.rs | 1069 ++++--- tests/user.rs | 49 +- tests/v2/project.rs | 911 +++--- tests/v2/scopes.rs | 184 +- tests/v2/search.rs | 530 ++-- tests/v2/tags.rs | 125 +- tests/v2/version.rs | 777 ++--- tests/version.rs | 1005 +++---- 51 files changed, 7604 insertions(+), 6662 deletions(-) create mode 100644 .sqlx/query-2efd0efe9ce16b2da01d9bcc1603e3a7ad0f9a1e5a457770608bc41dbb83f2dd.json create mode 100644 tests/common/api_common/generic.rs create mode 100644 tests/common/api_common/mod.rs create mode 100644 tests/common/api_common/models.rs diff --git a/.sqlx/query-2efd0efe9ce16b2da01d9bcc1603e3a7ad0f9a1e5a457770608bc41dbb83f2dd.json b/.sqlx/query-2efd0efe9ce16b2da01d9bcc1603e3a7ad0f9a1e5a457770608bc41dbb83f2dd.json new file mode 100644 index 00000000..e666764f --- /dev/null +++ b/.sqlx/query-2efd0efe9ce16b2da01d9bcc1603e3a7ad0f9a1e5a457770608bc41dbb83f2dd.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO payouts_values (user_id, mod_id, amount, created)\n SELECT * FROM UNNEST ($1::bigint[], $2::bigint[], $3::numeric[], $4::timestamptz[])\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8Array", + "Int8Array", + "NumericArray", + "TimestamptzArray" + ] + }, + "nullable": [] + }, + "hash": "2efd0efe9ce16b2da01d9bcc1603e3a7ad0f9a1e5a457770608bc41dbb83f2dd" +} diff --git a/Cargo.lock b/Cargo.lock index 06b62adc..1b27597b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2219,6 +2219,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json-patch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff1e1486799e3f64129f8ccad108b38290df9cd7015cd31bed17239f0789d6" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "treediff", +] + [[package]] name = "jsonwebtoken" version = "8.3.0" @@ -2267,6 +2279,7 @@ dependencies = [ "hyper-tls", "image", "itertools 0.11.0", + "json-patch", "lazy_static", "lettre", "log", @@ -4674,6 +4687,15 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "treediff" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52984d277bdf2a751072b5df30ec0377febdb02f7696d64c2d7d54630bac4303" +dependencies = [ + "serde_json", +] + [[package]] name = "try-lock" version = "0.2.4" diff --git a/Cargo.toml b/Cargo.toml index 040ed0c2..f2b881c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,7 +109,7 @@ derive-new = "0.5.9" [dev-dependencies] actix-http = "3.4.0" - +json-patch = "*" [profile.dev] opt-level = 0 # Minimal optimization, speeds up compilation lto = false # Disables Link Time Optimization diff --git a/src/models/v2/projects.rs b/src/models/v2/projects.rs index ba877b84..5e146c0b 100644 --- a/src/models/v2/projects.rs +++ b/src/models/v2/projects.rs @@ -233,10 +233,12 @@ pub struct LegacyVersion { /// and are now part of the dynamic fields system /// A list of game versions this project supports pub game_versions: Vec, - /// A list of loaders this project supports + + /// A list of loaders this project supports (has a newtype struct) pub loaders: Vec, - // TODO: remove this once we have v3 testing, as this is a v3 field and tests for it should be isolated to v3 + // TODO: should we remove this? as this is a v3 field and tests for it should be isolated to v3 + // it allows us to keep tests that use this struct in common pub ordering: Option, pub id: VersionId, diff --git a/src/routes/v2/version_creation.rs b/src/routes/v2/version_creation.rs index 4bfa9613..d55d851e 100644 --- a/src/routes/v2/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -105,7 +105,7 @@ pub async fn version_create( for file_part in &legacy_create.file_parts { if let Some(ext) = file_part.split('.').last() { match ext { - "mrpack" => { + "mrpack" | "mrpack-primary" => { project_type = Some("modpack"); break; } diff --git a/src/routes/v3/project_creation.rs b/src/routes/v3/project_creation.rs index 584225f4..2faee36e 100644 --- a/src/routes/v3/project_creation.rs +++ b/src/routes/v3/project_creation.rs @@ -894,7 +894,6 @@ async fn create_initial_version( &mut loader_field_enum_values, )?; - println!("Made it past here"); let dependencies = version_data .dependencies .iter() diff --git a/tests/analytics.rs b/tests/analytics.rs index c1f7806d..4f00258d 100644 --- a/tests/analytics.rs +++ b/tests/analytics.rs @@ -1,8 +1,12 @@ use actix_web::test; use chrono::{DateTime, Duration, Utc}; -use common::environment::TestEnvironment; use common::permissions::PermissionsTest; -use common::{database::*, permissions::PermissionsTestContext}; +use common::permissions::PermissionsTestContext; +use common::{ + api_v3::ApiV3, + database::*, + environment::{with_test_environment, TestEnvironment}, +}; use itertools::Itertools; use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::teams::ProjectPermissions; @@ -12,121 +16,120 @@ mod common; #[actix_rt::test] pub async fn analytics_revenue() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; - - let alpha_project_id = test_env - .dummy - .as_ref() - .unwrap() - .project_alpha - .project_id - .clone(); - - let pool = test_env.db.pool.clone(); - - // Generate sample revenue data- directly insert into sql - let (mut insert_user_ids, mut insert_project_ids, mut insert_payouts, mut insert_starts) = - (Vec::new(), Vec::new(), Vec::new(), Vec::new()); - - // Note: these go from most recent to least recent - let money_time_pairs: [(f64, DateTime); 10] = [ - (50.0, Utc::now() - Duration::minutes(5)), - (50.1, Utc::now() - Duration::minutes(10)), - (101.0, Utc::now() - Duration::days(1)), - (200.0, Utc::now() - Duration::days(2)), - (311.0, Utc::now() - Duration::days(3)), - (400.0, Utc::now() - Duration::days(4)), - (526.0, Utc::now() - Duration::days(5)), - (633.0, Utc::now() - Duration::days(6)), - (800.0, Utc::now() - Duration::days(14)), - (800.0, Utc::now() - Duration::days(800)), - ]; - - let project_id = parse_base62(&alpha_project_id).unwrap() as i64; - for (money, time) in money_time_pairs.iter() { - insert_user_ids.push(USER_USER_ID_PARSED); - insert_project_ids.push(project_id); - insert_payouts.push(Decimal::from_f64_retain(*money).unwrap()); - insert_starts.push(*time); - } - - sqlx::query!( - " - INSERT INTO payouts_values (user_id, mod_id, amount, created) - SELECT * FROM UNNEST ($1::bigint[], $2::bigint[], $3::numeric[], $4::timestamptz[]) - ", - &insert_user_ids[..], - &insert_project_ids[..], - &insert_payouts[..], - &insert_starts[..] - ) - .execute(&pool) - .await - .unwrap(); - - let day = 86400; - - // Test analytics endpoint with default values - // - all time points in the last 2 weeks - // - 1 day resolution - let analytics = api - .get_analytics_revenue_deserialized( - vec![&alpha_project_id], - false, - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(analytics.len(), 1); // 1 project - let project_analytics = analytics.get(&alpha_project_id).unwrap(); - assert_eq!(project_analytics.len(), 8); // 1 days cut off, and 2 points take place on the same day. note that the day exactly 14 days ago is included - // sorted_by_key, values in the order of smallest to largest key - let (sorted_keys, sorted_by_key): (Vec, Vec) = project_analytics - .iter() - .sorted_by_key(|(k, _)| *k) - .rev() - .unzip(); - assert_eq!( - vec![100.1, 101.0, 200.0, 311.0, 400.0, 526.0, 633.0, 800.0], - to_f64_vec_rounded_up(sorted_by_key) - ); - // Ensure that the keys are in multiples of 1 day - for k in sorted_keys { - assert_eq!(k % day, 0); - } - - // Test analytics with last 900 days to include all data - // keep resolution at default - let analytics = api - .get_analytics_revenue_deserialized( - vec![&alpha_project_id], - false, - Some(Utc::now() - Duration::days(801)), - None, - None, - USER_USER_PAT, + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + + let alpha_project_id = test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .project_id + .clone(); + + let pool = test_env.db.pool.clone(); + + // Generate sample revenue data- directly insert into sql + let (mut insert_user_ids, mut insert_project_ids, mut insert_payouts, mut insert_starts) = + (Vec::new(), Vec::new(), Vec::new(), Vec::new()); + + // Note: these go from most recent to least recent + let money_time_pairs: [(f64, DateTime); 10] = [ + (50.0, Utc::now() - Duration::minutes(5)), + (50.1, Utc::now() - Duration::minutes(10)), + (101.0, Utc::now() - Duration::days(1)), + (200.0, Utc::now() - Duration::days(2)), + (311.0, Utc::now() - Duration::days(3)), + (400.0, Utc::now() - Duration::days(4)), + (526.0, Utc::now() - Duration::days(5)), + (633.0, Utc::now() - Duration::days(6)), + (800.0, Utc::now() - Duration::days(14)), + (800.0, Utc::now() - Duration::days(800)), + ]; + + let project_id = parse_base62(&alpha_project_id).unwrap() as i64; + for (money, time) in money_time_pairs.iter() { + insert_user_ids.push(USER_USER_ID_PARSED); + insert_project_ids.push(project_id); + insert_payouts.push(Decimal::from_f64_retain(*money).unwrap()); + insert_starts.push(*time); + } + + sqlx::query!( + " + INSERT INTO payouts_values (user_id, mod_id, amount, created) + SELECT * FROM UNNEST ($1::bigint[], $2::bigint[], $3::numeric[], $4::timestamptz[]) + ", + &insert_user_ids[..], + &insert_project_ids[..], + &insert_payouts[..], + &insert_starts[..] ) - .await; - let project_analytics = analytics.get(&alpha_project_id).unwrap(); - assert_eq!(project_analytics.len(), 9); // and 2 points take place on the same day - let (sorted_keys, sorted_by_key): (Vec, Vec) = project_analytics - .iter() - .sorted_by_key(|(k, _)| *k) - .rev() - .unzip(); - assert_eq!( - vec![100.1, 101.0, 200.0, 311.0, 400.0, 526.0, 633.0, 800.0, 800.0], - to_f64_vec_rounded_up(sorted_by_key) - ); - for k in sorted_keys { - assert_eq!(k % day, 0); - } - - // Cleanup test db - test_env.cleanup().await; + .execute(&pool) + .await + .unwrap(); + + let day = 86400; + + // Test analytics endpoint with default values + // - all time points in the last 2 weeks + // - 1 day resolution + let analytics = api + .get_analytics_revenue_deserialized( + vec![&alpha_project_id], + false, + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(analytics.len(), 1); // 1 project + let project_analytics = analytics.get(&alpha_project_id).unwrap(); + assert_eq!(project_analytics.len(), 8); // 1 days cut off, and 2 points take place on the same day. note that the day exactly 14 days ago is included + // sorted_by_key, values in the order of smallest to largest key + let (sorted_keys, sorted_by_key): (Vec, Vec) = project_analytics + .iter() + .sorted_by_key(|(k, _)| *k) + .rev() + .unzip(); + assert_eq!( + vec![100.1, 101.0, 200.0, 311.0, 400.0, 526.0, 633.0, 800.0], + to_f64_vec_rounded_up(sorted_by_key) + ); + // Ensure that the keys are in multiples of 1 day + for k in sorted_keys { + assert_eq!(k % day, 0); + } + + // Test analytics with last 900 days to include all data + // keep resolution at default + let analytics = api + .get_analytics_revenue_deserialized( + vec![&alpha_project_id], + false, + Some(Utc::now() - Duration::days(801)), + None, + None, + USER_USER_PAT, + ) + .await; + let project_analytics = analytics.get(&alpha_project_id).unwrap(); + assert_eq!(project_analytics.len(), 9); // and 2 points take place on the same day + let (sorted_keys, sorted_by_key): (Vec, Vec) = project_analytics + .iter() + .sorted_by_key(|(k, _)| *k) + .rev() + .unzip(); + assert_eq!( + vec![100.1, 101.0, 200.0, 311.0, 400.0, 526.0, 633.0, 800.0, 800.0], + to_f64_vec_rounded_up(sorted_by_key) + ); + for k in sorted_keys { + assert_eq!(k % day, 0); + } + }) + .await; } fn to_f64_rounded_up(d: Decimal) -> f64 { @@ -141,89 +144,90 @@ fn to_f64_vec_rounded_up(d: Vec) -> Vec { #[actix_rt::test] pub async fn permissions_analytics_revenue() { - let test_env = TestEnvironment::build(None).await; - - let alpha_project_id = test_env - .dummy - .as_ref() - .unwrap() - .project_alpha - .project_id - .clone(); - let alpha_version_id = test_env - .dummy - .as_ref() - .unwrap() - .project_alpha - .version_id - .clone(); - let alpha_team_id = test_env - .dummy - .as_ref() - .unwrap() - .project_alpha - .team_id - .clone(); - - let view_analytics = ProjectPermissions::VIEW_ANALYTICS; - - // first, do check with a project - let req_gen = |ctx: &PermissionsTestContext| { - let projects_string = serde_json::to_string(&vec![ctx.project_id]).unwrap(); - let projects_string = urlencoding::encode(&projects_string); - test::TestRequest::get().uri(&format!( - "/v3/analytics/revenue?project_ids={projects_string}&resolution_minutes=5", - )) - }; - - PermissionsTest::new(&test_env) - .with_failure_codes(vec![200, 401]) - .with_200_json_checks( - // On failure, should have 0 projects returned - |value: &serde_json::Value| { - let value = value.as_object().unwrap(); - assert_eq!(value.len(), 0); - }, - // On success, should have 1 project returned - |value: &serde_json::Value| { - let value = value.as_object().unwrap(); - assert_eq!(value.len(), 1); - }, - ) - .simple_project_permissions_test(view_analytics, req_gen) - .await - .unwrap(); - - // Now with a version - // Need to use alpha - let req_gen = |_: &PermissionsTestContext| { - let versions_string = serde_json::to_string(&vec![alpha_version_id.clone()]).unwrap(); - let versions_string = urlencoding::encode(&versions_string); - test::TestRequest::get().uri(&format!( - "/v3/analytics/revenue?version_ids={versions_string}&resolution_minutes=5", - )) - }; - - PermissionsTest::new(&test_env) - .with_failure_codes(vec![200, 401]) - .with_existing_project(&alpha_project_id, &alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .with_200_json_checks( - // On failure, should have 0 versions returned - |value: &serde_json::Value| { - let value = value.as_object().unwrap(); - assert_eq!(value.len(), 0); - }, - // On success, should have 1 versions returned - |value: &serde_json::Value| { - let value = value.as_object().unwrap(); - assert_eq!(value.len(), 1); - }, - ) - .simple_project_permissions_test(view_analytics, req_gen) - .await - .unwrap(); - - // Cleanup test db - test_env.cleanup().await; + with_test_environment(None, |test_env: TestEnvironment| async move { + let alpha_project_id = test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .project_id + .clone(); + let alpha_version_id = test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .version_id + .clone(); + let alpha_team_id = test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .team_id + .clone(); + + let view_analytics = ProjectPermissions::VIEW_ANALYTICS; + + // first, do check with a project + let req_gen = |ctx: &PermissionsTestContext| { + let projects_string = serde_json::to_string(&vec![ctx.project_id]).unwrap(); + let projects_string = urlencoding::encode(&projects_string); + test::TestRequest::get().uri(&format!( + "/v3/analytics/revenue?project_ids={projects_string}&resolution_minutes=5", + )) + }; + + PermissionsTest::new(&test_env) + .with_failure_codes(vec![200, 401]) + .with_200_json_checks( + // On failure, should have 0 projects returned + |value: &serde_json::Value| { + let value = value.as_object().unwrap(); + assert_eq!(value.len(), 0); + }, + // On success, should have 1 project returned + |value: &serde_json::Value| { + let value = value.as_object().unwrap(); + assert_eq!(value.len(), 1); + }, + ) + .simple_project_permissions_test(view_analytics, req_gen) + .await + .unwrap(); + + // Now with a version + // Need to use alpha + let req_gen = |_: &PermissionsTestContext| { + let versions_string = serde_json::to_string(&vec![alpha_version_id.clone()]).unwrap(); + let versions_string = urlencoding::encode(&versions_string); + test::TestRequest::get().uri(&format!( + "/v3/analytics/revenue?version_ids={versions_string}&resolution_minutes=5", + )) + }; + + PermissionsTest::new(&test_env) + .with_failure_codes(vec![200, 401]) + .with_existing_project(&alpha_project_id, &alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .with_200_json_checks( + // On failure, should have 0 versions returned + |value: &serde_json::Value| { + let value = value.as_object().unwrap(); + assert_eq!(value.len(), 0); + }, + // On success, should have 1 versions returned + |value: &serde_json::Value| { + let value = value.as_object().unwrap(); + assert_eq!(value.len(), 1); + }, + ) + .simple_project_permissions_test(view_analytics, req_gen) + .await + .unwrap(); + + // Cleanup test db + test_env.cleanup().await; + }) + .await; } diff --git a/tests/common/api_common/generic.rs b/tests/common/api_common/generic.rs new file mode 100644 index 00000000..19b682b4 --- /dev/null +++ b/tests/common/api_common/generic.rs @@ -0,0 +1,136 @@ +use std::collections::HashMap; + +use actix_web::dev::ServiceResponse; +use async_trait::async_trait; +use labrinth::{ + models::{ + projects::{ProjectId, VersionType}, + teams::{OrganizationPermissions, ProjectPermissions}, + }, + search::SearchResults, +}; + +use crate::common::{api_v2::ApiV2, api_v3::ApiV3, dummy_data::TestFile}; + +use super::{ + models::{CommonImageData, CommonProject, CommonVersion}, + Api, ApiProject, ApiTags, ApiTeams, ApiVersion, +}; + +#[derive(Clone)] +pub enum GenericApi { + V2(ApiV2), + V3(ApiV3), +} + +macro_rules! delegate_api_variant { + ( + $(#[$meta:meta])* + impl $impl_name:ident for $struct_name:ident { + $( + [$method_name:ident, $ret:ty, $($param_name:ident: $param_type:ty),*] + ),* $(,)? + } + + ) => { + $(#[$meta])* + impl $impl_name for $struct_name { + $( + async fn $method_name(&self, $($param_name: $param_type),*) -> $ret { + match self { + $struct_name::V2(api) => api.$method_name($($param_name),*).await, + $struct_name::V3(api) => api.$method_name($($param_name),*).await, + } + } + )* + } + }; +} + +#[async_trait(?Send)] +impl Api for GenericApi { + async fn call(&self, req: actix_http::Request) -> ServiceResponse { + match self { + Self::V2(api) => api.call(req).await, + Self::V3(api) => api.call(req).await, + } + } + + async fn reset_search_index(&self) -> ServiceResponse { + match self { + Self::V2(api) => api.reset_search_index().await, + Self::V3(api) => api.reset_search_index().await, + } + } +} + +delegate_api_variant!( + #[async_trait(?Send)] + impl ApiProject for GenericApi { + [add_public_project, (CommonProject, Vec), slug: &str, version_jar: Option, modify_json: Option, pat: &str], + [remove_project, ServiceResponse, project_slug_or_id: &str, pat: &str], + [get_project, ServiceResponse, id_or_slug: &str, pat: &str], + [get_project_deserialized_common, CommonProject, id_or_slug: &str, pat: &str], + [get_user_projects, ServiceResponse, user_id_or_username: &str, pat: &str], + [get_user_projects_deserialized_common, Vec, user_id_or_username: &str, pat: &str], + [edit_project, ServiceResponse, id_or_slug: &str, patch: serde_json::Value, pat: &str], + [edit_project_bulk, ServiceResponse, ids_or_slugs: &[&str], patch: serde_json::Value, pat: &str], + [edit_project_icon, ServiceResponse, id_or_slug: &str, icon: Option, pat: &str], + [search_deserialized_common, SearchResults, query: Option<&str>, facets: Option, pat: &str], + } +); + +delegate_api_variant!( + #[async_trait(?Send)] + impl ApiTags for GenericApi { + [get_loaders, ServiceResponse,], + [get_loaders_deserialized_common, Vec,], + [get_categories, ServiceResponse,], + [get_categories_deserialized_common, Vec,], + } +); + +delegate_api_variant!( + #[async_trait(?Send)] + impl ApiTeams for GenericApi { + [get_team_members, ServiceResponse, team_id: &str, pat: &str], + [get_team_members_deserialized_common, Vec, team_id: &str, pat: &str], + [get_project_members, ServiceResponse, id_or_slug: &str, pat: &str], + [get_project_members_deserialized_common, Vec, id_or_slug: &str, pat: &str], + [get_organization_members, ServiceResponse, id_or_title: &str, pat: &str], + [get_organization_members_deserialized_common, Vec, id_or_title: &str, pat: &str], + [join_team, ServiceResponse, team_id: &str, pat: &str], + [remove_from_team, ServiceResponse, team_id: &str, user_id: &str, pat: &str], + [edit_team_member, ServiceResponse, team_id: &str, user_id: &str, patch: serde_json::Value, pat: &str], + [transfer_team_ownership, ServiceResponse, team_id: &str, user_id: &str, pat: &str], + [get_user_notifications, ServiceResponse, user_id: &str, pat: &str], + [get_user_notifications_deserialized_common, Vec, user_id: &str, pat: &str], + [mark_notification_read, ServiceResponse, notification_id: &str, pat: &str], + [add_user_to_team, ServiceResponse, team_id: &str, user_id: &str, project_permissions: Option, organization_permissions: Option, pat: &str], + [delete_notification, ServiceResponse, notification_id: &str, pat: &str], + } +); + +delegate_api_variant!( + #[async_trait(?Send)] + impl ApiVersion for GenericApi { + [add_public_version, ServiceResponse, project_id: ProjectId, version_number: &str, version_jar: TestFile, ordering: Option, modify_json: Option, pat: &str], + [add_public_version_deserialized_common, CommonVersion, project_id: ProjectId, version_number: &str, version_jar: TestFile, ordering: Option, modify_json: Option, pat: &str], + [get_version, ServiceResponse, id_or_slug: &str, pat: &str], + [get_version_deserialized_common, CommonVersion, id_or_slug: &str, pat: &str], + [get_versions, ServiceResponse, ids_or_slugs: Vec, pat: &str], + [get_versions_deserialized_common, Vec, ids_or_slugs: Vec, pat: &str], + [edit_version, ServiceResponse, id_or_slug: &str, patch: serde_json::Value, pat: &str], + [get_version_from_hash, ServiceResponse, id_or_slug: &str, hash: &str, pat: &str], + [get_version_from_hash_deserialized_common, CommonVersion, id_or_slug: &str, hash: &str, pat: &str], + [get_versions_from_hashes, ServiceResponse, hashes: &[&str], algorithm: &str, pat: &str], + [get_versions_from_hashes_deserialized_common, HashMap, hashes: &[&str], algorithm: &str, pat: &str], + [get_update_from_hash, ServiceResponse, hash: &str, algorithm: &str, loaders: Option>,game_versions: Option>, version_types: Option>, pat: &str], + [get_update_from_hash_deserialized_common, CommonVersion, hash: &str, algorithm: &str,loaders: Option>,game_versions: Option>,version_types: Option>, pat: &str], + [update_files, ServiceResponse, algorithm: &str, hashes: Vec, loaders: Option>, game_versions: Option>, version_types: Option>, pat: &str], + [update_files_deserialized_common, HashMap, algorithm: &str, hashes: Vec, loaders: Option>, game_versions: Option>, version_types: Option>, pat: &str], + [get_project_versions, ServiceResponse, project_id_slug: &str, game_versions: Option>,loaders: Option>,featured: Option, version_type: Option, limit: Option, offset: Option,pat: &str], + [get_project_versions_deserialized_common, Vec, project_id_slug: &str, game_versions: Option>, loaders: Option>,featured: Option,version_type: Option,limit: Option,offset: Option,pat: &str], + [edit_version_ordering, ServiceResponse, version_id: &str,ordering: Option,pat: &str], + } +); diff --git a/tests/common/api_common/mod.rs b/tests/common/api_common/mod.rs new file mode 100644 index 00000000..abf38293 --- /dev/null +++ b/tests/common/api_common/mod.rs @@ -0,0 +1,262 @@ +use std::collections::HashMap; + +use self::models::{ + CommonCategoryData, CommonImageData, CommonLoaderData, CommonNotification, CommonProject, + CommonTeamMember, CommonVersion, +}; +use actix_web::dev::ServiceResponse; +use async_trait::async_trait; +use labrinth::{ + models::{ + projects::{ProjectId, VersionType}, + teams::{OrganizationPermissions, ProjectPermissions}, + }, + search::SearchResults, + LabrinthConfig, +}; + +use super::dummy_data::TestFile; + +pub mod generic; +pub mod models; +#[async_trait(?Send)] +pub trait ApiBuildable: Api { + async fn build(labrinth_config: LabrinthConfig) -> Self; +} + +#[async_trait(?Send)] +pub trait Api: ApiProject + ApiTags + ApiTeams + ApiVersion { + async fn call(&self, req: actix_http::Request) -> ServiceResponse; + async fn reset_search_index(&self) -> ServiceResponse; +} + +#[async_trait(?Send)] +pub trait ApiProject { + async fn add_public_project( + &self, + slug: &str, + version_jar: Option, + modify_json: Option, + pat: &str, + ) -> (CommonProject, Vec); + async fn remove_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse; + async fn get_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse; + async fn get_project_deserialized_common(&self, id_or_slug: &str, pat: &str) -> CommonProject; + async fn get_user_projects(&self, user_id_or_username: &str, pat: &str) -> ServiceResponse; + async fn get_user_projects_deserialized_common( + &self, + user_id_or_username: &str, + pat: &str, + ) -> Vec; + async fn edit_project( + &self, + id_or_slug: &str, + patch: serde_json::Value, + pat: &str, + ) -> ServiceResponse; + async fn edit_project_bulk( + &self, + ids_or_slugs: &[&str], + patch: serde_json::Value, + pat: &str, + ) -> ServiceResponse; + async fn edit_project_icon( + &self, + id_or_slug: &str, + icon: Option, + pat: &str, + ) -> ServiceResponse; + async fn search_deserialized_common( + &self, + query: Option<&str>, + facets: Option, + pat: &str, + ) -> SearchResults; +} + +#[async_trait(?Send)] +pub trait ApiTags { + async fn get_loaders(&self) -> ServiceResponse; + async fn get_loaders_deserialized_common(&self) -> Vec; + async fn get_categories(&self) -> ServiceResponse; + async fn get_categories_deserialized_common(&self) -> Vec; +} + +#[async_trait(?Send)] +pub trait ApiTeams { + async fn get_team_members(&self, team_id: &str, pat: &str) -> ServiceResponse; + async fn get_team_members_deserialized_common( + &self, + team_id: &str, + pat: &str, + ) -> Vec; + async fn get_project_members(&self, id_or_slug: &str, pat: &str) -> ServiceResponse; + async fn get_project_members_deserialized_common( + &self, + id_or_slug: &str, + pat: &str, + ) -> Vec; + async fn get_organization_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse; + async fn get_organization_members_deserialized_common( + &self, + id_or_title: &str, + pat: &str, + ) -> Vec; + async fn join_team(&self, team_id: &str, pat: &str) -> ServiceResponse; + async fn remove_from_team(&self, team_id: &str, user_id: &str, pat: &str) -> ServiceResponse; + async fn edit_team_member( + &self, + team_id: &str, + user_id: &str, + patch: serde_json::Value, + pat: &str, + ) -> ServiceResponse; + async fn transfer_team_ownership( + &self, + team_id: &str, + user_id: &str, + pat: &str, + ) -> ServiceResponse; + async fn get_user_notifications(&self, user_id: &str, pat: &str) -> ServiceResponse; + async fn get_user_notifications_deserialized_common( + &self, + user_id: &str, + pat: &str, + ) -> Vec; + async fn mark_notification_read(&self, notification_id: &str, pat: &str) -> ServiceResponse; + async fn add_user_to_team( + &self, + team_id: &str, + user_id: &str, + project_permissions: Option, + organization_permissions: Option, + pat: &str, + ) -> ServiceResponse; + async fn delete_notification(&self, notification_id: &str, pat: &str) -> ServiceResponse; +} + +#[async_trait(?Send)] +pub trait ApiVersion { + async fn add_public_version( + &self, + project_id: ProjectId, + version_number: &str, + version_jar: TestFile, + ordering: Option, + modify_json: Option, + pat: &str, + ) -> ServiceResponse; + async fn add_public_version_deserialized_common( + &self, + project_id: ProjectId, + version_number: &str, + version_jar: TestFile, + ordering: Option, + modify_json: Option, + pat: &str, + ) -> CommonVersion; + async fn get_version(&self, id_or_slug: &str, pat: &str) -> ServiceResponse; + async fn get_version_deserialized_common(&self, id_or_slug: &str, pat: &str) -> CommonVersion; + async fn get_versions(&self, ids_or_slugs: Vec, pat: &str) -> ServiceResponse; + async fn get_versions_deserialized_common( + &self, + ids_or_slugs: Vec, + pat: &str, + ) -> Vec; + async fn edit_version( + &self, + id_or_slug: &str, + patch: serde_json::Value, + pat: &str, + ) -> ServiceResponse; + async fn get_version_from_hash( + &self, + id_or_slug: &str, + hash: &str, + pat: &str, + ) -> ServiceResponse; + async fn get_version_from_hash_deserialized_common( + &self, + id_or_slug: &str, + hash: &str, + pat: &str, + ) -> CommonVersion; + async fn get_versions_from_hashes( + &self, + hashes: &[&str], + algorithm: &str, + pat: &str, + ) -> ServiceResponse; + async fn get_versions_from_hashes_deserialized_common( + &self, + hashes: &[&str], + algorithm: &str, + pat: &str, + ) -> HashMap; + async fn get_update_from_hash( + &self, + hash: &str, + algorithm: &str, + loaders: Option>, + game_versions: Option>, + version_types: Option>, + pat: &str, + ) -> ServiceResponse; + async fn get_update_from_hash_deserialized_common( + &self, + hash: &str, + algorithm: &str, + loaders: Option>, + game_versions: Option>, + version_types: Option>, + pat: &str, + ) -> CommonVersion; + async fn update_files( + &self, + algorithm: &str, + hashes: Vec, + loaders: Option>, + game_versions: Option>, + version_types: Option>, + pat: &str, + ) -> ServiceResponse; + async fn update_files_deserialized_common( + &self, + algorithm: &str, + hashes: Vec, + loaders: Option>, + game_versions: Option>, + version_types: Option>, + pat: &str, + ) -> HashMap; + #[allow(clippy::too_many_arguments)] + async fn get_project_versions( + &self, + project_id_slug: &str, + game_versions: Option>, + loaders: Option>, + featured: Option, + version_type: Option, + limit: Option, + offset: Option, + pat: &str, + ) -> ServiceResponse; + #[allow(clippy::too_many_arguments)] + async fn get_project_versions_deserialized_common( + &self, + slug: &str, + game_versions: Option>, + loaders: Option>, + featured: Option, + version_type: Option, + limit: Option, + offset: Option, + pat: &str, + ) -> Vec; + async fn edit_version_ordering( + &self, + version_id: &str, + ordering: Option, + pat: &str, + ) -> ServiceResponse; +} diff --git a/tests/common/api_common/models.rs b/tests/common/api_common/models.rs new file mode 100644 index 00000000..40fbb8ab --- /dev/null +++ b/tests/common/api_common/models.rs @@ -0,0 +1,141 @@ +use chrono::{DateTime, Utc}; +use labrinth::models::{ + notifications::{NotificationAction, NotificationBody, NotificationId}, + organizations::OrganizationId, + projects::{ + Dependency, DonationLink, GalleryItem, License, ModeratorMessage, MonetizationStatus, + ProjectId, ProjectStatus, VersionFile, VersionId, VersionStatus, VersionType, + }, + teams::{OrganizationPermissions, ProjectPermissions, TeamId}, + threads::ThreadId, + users::{User, UserId}, +}; +use rust_decimal::Decimal; +use serde::Deserialize; + +// Fields shared by every version of the API. +// No struct in here should have ANY field that +// is not present in *every* version of the API. + +// These are used for common tests- tests that can be used on both V2 AND v3 of the API and have the same results. + +// Any test that requires version-specific fields should have its own test that is not done for each version, +// as the environment generator for both uses common fields. + +#[derive(Deserialize)] +pub struct CommonProject { + // For example, for CommonProject, we do not include: + // - game_versions (v2 only) + // - loader_fields (v3 only) + // - etc. + // For any tests that require those fields, we make a separate test with separate API functions tht do not use Common models. + pub id: ProjectId, + pub slug: Option, + pub team: TeamId, + pub organization: Option, + pub title: String, + pub description: String, + pub body: String, + pub body_url: Option, + pub published: DateTime, + pub updated: DateTime, + pub approved: Option>, + pub queued: Option>, + pub status: ProjectStatus, + pub requested_status: Option, + pub moderator_message: Option, + pub license: License, + pub downloads: u32, + pub followers: u32, + pub categories: Vec, + pub additional_categories: Vec, + pub loaders: Vec, + pub versions: Vec, + pub icon_url: Option, + pub issues_url: Option, + pub source_url: Option, + pub wiki_url: Option, + pub discord_url: Option, + pub donation_urls: Option>, + pub gallery: Vec, + pub color: Option, + pub thread_id: ThreadId, + pub monetization_status: MonetizationStatus, +} +#[derive(Deserialize, Clone)] +pub struct CommonVersion { + pub id: VersionId, + pub loaders: Vec, + pub project_id: ProjectId, + pub author_id: UserId, + pub featured: bool, + pub name: String, + pub version_number: String, + pub changelog: String, + pub changelog_url: Option, + pub date_published: DateTime, + pub downloads: u32, + pub version_type: VersionType, + pub status: VersionStatus, + pub requested_status: Option, + pub files: Vec, + pub dependencies: Vec, + + // TODO: should ordering be in v2? + pub ordering: Option, +} + +#[derive(Deserialize)] +pub struct CommonImageData { + pub filename: String, + pub extension: String, + pub icon: Vec, +} + +#[derive(Deserialize)] +pub struct CommonLoaderData { + pub icon: String, + pub name: String, + pub supported_project_types: Vec, +} + +#[derive(Deserialize)] +pub struct CommonCategoryData { + pub icon: String, + pub name: String, + pub project_type: String, + pub header: String, +} + +/// A member of a team +#[derive(Deserialize)] +pub struct CommonTeamMember { + pub team_id: TeamId, + pub user: User, + pub role: String, + + // TODO: Should these be removed from the Common? + pub permissions: Option, + pub organization_permissions: Option, + + pub accepted: bool, + pub payouts_split: Option, + pub ordering: i64, +} + +#[derive(Deserialize)] +pub struct CommonNotification { + pub id: NotificationId, + pub user_id: UserId, + pub read: bool, + pub created: DateTime, + pub body: NotificationBody, + + // DEPRECATED: use body field instead + #[serde(rename = "type")] + pub type_: Option, + pub title: String, + pub text: String, + pub link: String, + pub actions: Vec, +} diff --git a/tests/common/api_v2/mod.rs b/tests/common/api_v2/mod.rs index 8f5f470f..2ca30635 100644 --- a/tests/common/api_v2/mod.rs +++ b/tests/common/api_v2/mod.rs @@ -1,7 +1,12 @@ #![allow(dead_code)] -use super::environment::LocalService; -use actix_web::dev::ServiceResponse; +use super::{ + api_common::{Api, ApiBuildable}, + environment::LocalService, +}; +use actix_web::{dev::ServiceResponse, test, App}; +use async_trait::async_trait; +use labrinth::LabrinthConfig; use std::rc::Rc; pub mod project; @@ -15,12 +20,23 @@ pub struct ApiV2 { pub test_app: Rc, } -impl ApiV2 { - pub async fn call(&self, req: actix_http::Request) -> ServiceResponse { +#[async_trait(?Send)] +impl ApiBuildable for ApiV2 { + async fn build(labrinth_config: LabrinthConfig) -> Self { + let app = App::new().configure(|cfg| labrinth::app_config(cfg, labrinth_config.clone())); + let test_app: Rc = Rc::new(test::init_service(app).await); + + Self { test_app } + } +} + +#[async_trait(?Send)] +impl Api for ApiV2 { + async fn call(&self, req: actix_http::Request) -> ServiceResponse { self.test_app.call(req).await.unwrap() } - pub async fn reset_search_index(&self) -> ServiceResponse { + async fn reset_search_index(&self) -> ServiceResponse { let req = actix_web::test::TestRequest::post() .uri("/v2/admin/_force_reindex") .append_header(( diff --git a/tests/common/api_v2/project.rs b/tests/common/api_v2/project.rs index 10f910d2..f28c048c 100644 --- a/tests/common/api_v2/project.rs +++ b/tests/common/api_v2/project.rs @@ -1,30 +1,55 @@ -use crate::common::api_v2::request_data::ProjectCreationRequestData; +use crate::common::{ + api_common::{ + models::{CommonImageData, CommonProject, CommonVersion}, + Api, ApiProject, + }, + dummy_data::TestFile, +}; use actix_http::StatusCode; use actix_web::{ dev::ServiceResponse, test::{self, TestRequest}, }; +use async_trait::async_trait; use bytes::Bytes; -use chrono::{DateTime, Utc}; use labrinth::{ - models::v2::projects::{LegacyProject, LegacyVersion}, - search::SearchResults, - util::actix::AppendsMultipart, + models::v2::projects::LegacyProject, search::SearchResults, util::actix::AppendsMultipart, }; -use rust_decimal::Decimal; use serde_json::json; -use std::collections::HashMap; use crate::common::{asserts::assert_status, database::MOD_USER_PAT}; -use super::{request_data::ImageData, ApiV2}; +use super::{request_data::get_public_project_creation_data, ApiV2}; impl ApiV2 { - pub async fn add_public_project( + pub async fn get_project_deserialized(&self, id_or_slug: &str, pat: &str) -> LegacyProject { + let resp = self.get_project(id_or_slug, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn get_user_projects_deserialized( &self, - creation_data: ProjectCreationRequestData, + user_id_or_username: &str, pat: &str, - ) -> (LegacyProject, Vec) { + ) -> Vec { + let resp = self.get_user_projects(user_id_or_username, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } +} + +#[async_trait(?Send)] +impl ApiProject for ApiV2 { + async fn add_public_project( + &self, + slug: &str, + version_jar: Option, + modify_json: Option, + pat: &str, + ) -> (CommonProject, Vec) { + let creation_data = get_public_project_creation_data(slug, version_jar, modify_json); + // Add a project. let req = TestRequest::post() .uri("/v2/project") @@ -48,7 +73,7 @@ impl ApiV2 { assert_status(&resp, StatusCode::NO_CONTENT); let project = self - .get_project_deserialized(&creation_data.slug, pat) + .get_project_deserialized_common(&creation_data.slug, pat) .await; // Get project's versions @@ -57,12 +82,12 @@ impl ApiV2 { .append_header(("Authorization", pat)) .to_request(); let resp = self.call(req).await; - let versions: Vec = test::read_body_json(resp).await; + let versions: Vec = test::read_body_json(resp).await; (project, versions) } - pub async fn remove_project(&self, project_slug_or_id: &str, pat: &str) -> ServiceResponse { + async fn remove_project(&self, project_slug_or_id: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v2/project/{project_slug_or_id}")) .append_header(("Authorization", pat)) @@ -72,20 +97,21 @@ impl ApiV2 { resp } - pub async fn get_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse { + async fn get_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v2/project/{id_or_slug}")) .append_header(("Authorization", pat)) .to_request(); self.call(req).await } - pub async fn get_project_deserialized(&self, id_or_slug: &str, pat: &str) -> LegacyProject { + + async fn get_project_deserialized_common(&self, id_or_slug: &str, pat: &str) -> CommonProject { let resp = self.get_project(id_or_slug, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_user_projects(&self, user_id_or_username: &str, pat: &str) -> ServiceResponse { + async fn get_user_projects(&self, user_id_or_username: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/user/{}/projects", user_id_or_username)) .append_header(("Authorization", pat)) @@ -93,17 +119,17 @@ impl ApiV2 { self.call(req).await } - pub async fn get_user_projects_deserialized( + async fn get_user_projects_deserialized_common( &self, user_id_or_username: &str, pat: &str, - ) -> Vec { + ) -> Vec { let resp = self.get_user_projects(user_id_or_username, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn edit_project( + async fn edit_project( &self, id_or_slug: &str, patch: serde_json::Value, @@ -118,14 +144,14 @@ impl ApiV2 { self.call(req).await } - pub async fn edit_project_bulk( + async fn edit_project_bulk( &self, - ids_or_slugs: impl IntoIterator, + ids_or_slugs: &[&str], patch: serde_json::Value, pat: &str, ) -> ServiceResponse { let projects_str = ids_or_slugs - .into_iter() + .iter() .map(|s| format!("\"{}\"", s)) .collect::>() .join(","); @@ -141,10 +167,10 @@ impl ApiV2 { self.call(req).await } - pub async fn edit_project_icon( + async fn edit_project_icon( &self, id_or_slug: &str, - icon: Option, + icon: Option, pat: &str, ) -> ServiceResponse { if let Some(icon) = icon { @@ -170,7 +196,7 @@ impl ApiV2 { } } - pub async fn search_deserialized( + async fn search_deserialized_common( &self, query: Option<&str>, facets: Option, @@ -197,57 +223,4 @@ impl ApiV2 { assert_eq!(status, 200); test::read_body_json(resp).await } - - pub async fn get_analytics_revenue( - &self, - id_or_slugs: Vec<&str>, - start_date: Option>, - end_date: Option>, - resolution_minutes: Option, - pat: &str, - ) -> ServiceResponse { - let projects_string = serde_json::to_string(&id_or_slugs).unwrap(); - let projects_string = urlencoding::encode(&projects_string); - - let mut extra_args = String::new(); - if let Some(start_date) = start_date { - let start_date = start_date.to_rfc3339(); - // let start_date = serde_json::to_string(&start_date).unwrap(); - let start_date = urlencoding::encode(&start_date); - extra_args.push_str(&format!("&start_date={start_date}")); - } - if let Some(end_date) = end_date { - let end_date = end_date.to_rfc3339(); - // let end_date = serde_json::to_string(&end_date).unwrap(); - let end_date = urlencoding::encode(&end_date); - extra_args.push_str(&format!("&end_date={end_date}")); - } - if let Some(resolution_minutes) = resolution_minutes { - extra_args.push_str(&format!("&resolution_minutes={}", resolution_minutes)); - } - - let req = test::TestRequest::get() - .uri(&format!( - "/v2/analytics/revenue?{projects_string}{extra_args}", - )) - .append_header(("Authorization", pat)) - .to_request(); - - self.call(req).await - } - - pub async fn get_analytics_revenue_deserialized( - &self, - id_or_slugs: Vec<&str>, - start_date: Option>, - end_date: Option>, - resolution_minutes: Option, - pat: &str, - ) -> HashMap> { - let resp = self - .get_analytics_revenue(id_or_slugs, start_date, end_date, resolution_minutes, pat) - .await; - assert_eq!(resp.status(), 200); - test::read_body_json(resp).await - } } diff --git a/tests/common/api_v2/request_data.rs b/tests/common/api_v2/request_data.rs index a7a84c30..e0175d90 100644 --- a/tests/common/api_v2/request_data.rs +++ b/tests/common/api_v2/request_data.rs @@ -28,8 +28,12 @@ pub struct ImageData { pub fn get_public_project_creation_data( slug: &str, version_jar: Option, + modify_json: Option, ) -> ProjectCreationRequestData { - let json_data = get_public_project_creation_data_json(slug, version_jar.as_ref()); + let mut json_data = get_public_project_creation_data_json(slug, version_jar.as_ref()); + if let Some(modify_json) = modify_json { + json_patch::patch(&mut json_data, &modify_json).unwrap(); + } let multipart_data = get_public_creation_data_multipart(&json_data, version_jar.as_ref()); ProjectCreationRequestData { slug: slug.to_string(), @@ -42,9 +46,15 @@ pub fn get_public_version_creation_data( project_id: ProjectId, version_number: &str, version_jar: TestFile, + ordering: Option, + modify_json: Option, ) -> VersionCreationRequestData { - let mut json_data = get_public_version_creation_data_json(version_number, &version_jar); + let mut json_data = + get_public_version_creation_data_json(version_number, ordering, &version_jar); json_data["project_id"] = json!(project_id); + if let Some(modify_json) = modify_json { + json_patch::patch(&mut json_data, &modify_json).unwrap(); + } let multipart_data = get_public_creation_data_multipart(&json_data, Some(&version_jar)); VersionCreationRequestData { version: version_number.to_string(), @@ -55,9 +65,10 @@ pub fn get_public_version_creation_data( pub fn get_public_version_creation_data_json( version_number: &str, + ordering: Option, version_jar: &TestFile, ) -> serde_json::Value { - json!({ + let mut j = json!({ "file_parts": [version_jar.filename()], "version_number": version_number, "version_title": "start", @@ -66,7 +77,11 @@ pub fn get_public_version_creation_data_json( "release_channel": "release", "loaders": ["fabric"], "featured": true - }) + }); + if let Some(ordering) = ordering { + j["ordering"] = json!(ordering); + } + j } pub fn get_public_project_creation_data_json( @@ -74,7 +89,7 @@ pub fn get_public_project_creation_data_json( version_jar: Option<&TestFile>, ) -> serde_json::Value { let initial_versions = if let Some(jar) = version_jar { - json!([get_public_version_creation_data_json("1.2.3", jar)]) + json!([get_public_version_creation_data_json("1.2.3", None, jar)]) } else { json!([]) }; diff --git a/tests/common/api_v2/tags.rs b/tests/common/api_v2/tags.rs index f220b17b..6b41930b 100644 --- a/tests/common/api_v2/tags.rs +++ b/tests/common/api_v2/tags.rs @@ -2,16 +2,23 @@ use actix_web::{ dev::ServiceResponse, test::{self, TestRequest}, }; +use async_trait::async_trait; use labrinth::routes::v2::tags::{CategoryData, GameVersionQueryData, LoaderData}; -use crate::common::database::ADMIN_USER_PAT; +use crate::common::{ + api_common::{ + models::{CommonCategoryData, CommonLoaderData}, + Api, ApiTags, + }, + database::ADMIN_USER_PAT, +}; use super::ApiV2; -impl ApiV2 { - // Tag gets do not include PAT, as they are public. +// TODO: Tag gets do not include PAT, as they are public. - pub async fn get_side_types(&self) -> ServiceResponse { +impl ApiV2 { + async fn get_side_types(&self) -> ServiceResponse { let req = TestRequest::get() .uri("/v2/tag/side_type") .append_header(("Authorization", ADMIN_USER_PAT)) @@ -25,44 +32,59 @@ impl ApiV2 { test::read_body_json(resp).await } - pub async fn get_loaders(&self) -> ServiceResponse { + pub async fn get_game_versions(&self) -> ServiceResponse { let req = TestRequest::get() - .uri("/v2/tag/loader") + .uri("/v2/tag/game_version") .append_header(("Authorization", ADMIN_USER_PAT)) .to_request(); self.call(req).await } + pub async fn get_game_versions_deserialized(&self) -> Vec { + let resp = self.get_game_versions().await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + pub async fn get_loaders_deserialized(&self) -> Vec { let resp = self.get_loaders().await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_categories(&self) -> ServiceResponse { + pub async fn get_categories_deserialized(&self) -> Vec { + let resp = self.get_categories().await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } +} + +#[async_trait(?Send)] +impl ApiTags for ApiV2 { + async fn get_loaders(&self) -> ServiceResponse { let req = TestRequest::get() - .uri("/v2/tag/category") + .uri("/v2/tag/loader") .append_header(("Authorization", ADMIN_USER_PAT)) .to_request(); self.call(req).await } - pub async fn get_categories_deserialized(&self) -> Vec { - let resp = self.get_categories().await; + async fn get_loaders_deserialized_common(&self) -> Vec { + let resp = self.get_loaders().await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_game_versions(&self) -> ServiceResponse { + async fn get_categories(&self) -> ServiceResponse { let req = TestRequest::get() - .uri("/v2/tag/game_version") + .uri("/v2/tag/category") .append_header(("Authorization", ADMIN_USER_PAT)) .to_request(); self.call(req).await } - pub async fn get_game_versions_deserialized(&self) -> Vec { - let resp = self.get_game_versions().await; + async fn get_categories_deserialized_common(&self) -> Vec { + let resp = self.get_categories().await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } diff --git a/tests/common/api_v2/team.rs b/tests/common/api_v2/team.rs index 1a772053..0b16f510 100644 --- a/tests/common/api_v2/team.rs +++ b/tests/common/api_v2/team.rs @@ -1,17 +1,22 @@ use actix_http::StatusCode; use actix_web::{dev::ServiceResponse, test}; -use labrinth::models::{ - notifications::Notification, - teams::{OrganizationPermissions, ProjectPermissions, TeamMember}, -}; +use async_trait::async_trait; +use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; use serde_json::json; -use crate::common::asserts::assert_status; +use crate::common::{ + api_common::{ + models::{CommonNotification, CommonTeamMember}, + Api, ApiTeams, + }, + asserts::assert_status, +}; use super::ApiV2; -impl ApiV2 { - pub async fn get_team_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { +#[async_trait(?Send)] +impl ApiTeams for ApiV2 { + async fn get_team_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/team/{id_or_title}/members")) .append_header(("Authorization", pat)) @@ -19,17 +24,17 @@ impl ApiV2 { self.call(req).await } - pub async fn get_team_members_deserialized( + async fn get_team_members_deserialized_common( &self, id_or_title: &str, pat: &str, - ) -> Vec { + ) -> Vec { let resp = self.get_team_members(id_or_title, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_project_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + async fn get_project_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/project/{id_or_title}/members")) .append_header(("Authorization", pat)) @@ -37,17 +42,17 @@ impl ApiV2 { self.call(req).await } - pub async fn get_project_members_deserialized( + async fn get_project_members_deserialized_common( &self, id_or_title: &str, pat: &str, - ) -> Vec { + ) -> Vec { let resp = self.get_project_members(id_or_title, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_organization_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + async fn get_organization_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/organization/{id_or_title}/members")) .append_header(("Authorization", pat)) @@ -55,17 +60,17 @@ impl ApiV2 { self.call(req).await } - pub async fn get_organization_members_deserialized( + async fn get_organization_members_deserialized_common( &self, id_or_title: &str, pat: &str, - ) -> Vec { + ) -> Vec { let resp = self.get_organization_members(id_or_title, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn join_team(&self, team_id: &str, pat: &str) -> ServiceResponse { + async fn join_team(&self, team_id: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::post() .uri(&format!("/v2/team/{team_id}/join")) .append_header(("Authorization", pat)) @@ -73,12 +78,7 @@ impl ApiV2 { self.call(req).await } - pub async fn remove_from_team( - &self, - team_id: &str, - user_id: &str, - pat: &str, - ) -> ServiceResponse { + async fn remove_from_team(&self, team_id: &str, user_id: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v2/team/{team_id}/members/{user_id}")) .append_header(("Authorization", pat)) @@ -86,7 +86,7 @@ impl ApiV2 { self.call(req).await } - pub async fn edit_team_member( + async fn edit_team_member( &self, team_id: &str, user_id: &str, @@ -101,7 +101,7 @@ impl ApiV2 { self.call(req).await } - pub async fn transfer_team_ownership( + async fn transfer_team_ownership( &self, team_id: &str, user_id: &str, @@ -117,7 +117,7 @@ impl ApiV2 { self.call(req).await } - pub async fn get_user_notifications(&self, user_id: &str, pat: &str) -> ServiceResponse { + async fn get_user_notifications(&self, user_id: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v2/user/{user_id}/notifications")) .append_header(("Authorization", pat)) @@ -125,28 +125,25 @@ impl ApiV2 { self.call(req).await } - pub async fn get_user_notifications_deserialized( + async fn get_user_notifications_deserialized_common( &self, user_id: &str, pat: &str, - ) -> Vec { + ) -> Vec { let resp = self.get_user_notifications(user_id, pat).await; assert_status(&resp, StatusCode::OK); test::read_body_json(resp).await } - pub async fn mark_notification_read( - &self, - notification_id: &str, - pat: &str, - ) -> ServiceResponse { + async fn mark_notification_read(&self, notification_id: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::patch() .uri(&format!("/v2/notification/{notification_id}")) .append_header(("Authorization", pat)) .to_request(); self.call(req).await } - pub async fn add_user_to_team( + + async fn add_user_to_team( &self, team_id: &str, user_id: &str, @@ -166,7 +163,7 @@ impl ApiV2 { self.call(req).await } - pub async fn delete_notification(&self, notification_id: &str, pat: &str) -> ServiceResponse { + async fn delete_notification(&self, notification_id: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v2/notification/{notification_id}")) .append_header(("Authorization", pat)) diff --git a/tests/common/api_v2/version.rs b/tests/common/api_v2/version.rs index 935a5b7f..5e94c8e4 100644 --- a/tests/common/api_v2/version.rs +++ b/tests/common/api_v2/version.rs @@ -1,60 +1,142 @@ use std::collections::HashMap; +use super::{request_data::get_public_version_creation_data, ApiV2}; +use crate::common::{ + api_common::{models::CommonVersion, Api, ApiVersion}, + asserts::assert_status, + dummy_data::TestFile, +}; use actix_http::{header::AUTHORIZATION, StatusCode}; use actix_web::{ dev::ServiceResponse, test::{self, TestRequest}, }; +use async_trait::async_trait; use labrinth::{ - models::{projects::VersionType, v2::projects::LegacyVersion}, + models::{ + projects::{ProjectId, VersionType}, + v2::projects::LegacyVersion, + }, routes::v2::version_file::FileUpdateData, util::actix::AppendsMultipart, }; use serde_json::json; -use crate::common::asserts::assert_status; - -use super::{request_data::VersionCreationRequestData, ApiV2}; - pub fn url_encode_json_serialized_vec(elements: &[String]) -> String { let serialized = serde_json::to_string(&elements).unwrap(); urlencoding::encode(&serialized).to_string() } impl ApiV2 { - pub async fn add_public_version( + pub async fn get_version_deserialized(&self, id: &str, pat: &str) -> LegacyVersion { + let resp = self.get_version(id, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn get_version_from_hash_deserialized( &self, - creation_data: VersionCreationRequestData, + hash: &str, + algorithm: &str, pat: &str, ) -> LegacyVersion { + let resp = self.get_version_from_hash(hash, algorithm, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn get_versions_from_hashes_deserialized( + &self, + hashes: &[&str], + algorithm: &str, + pat: &str, + ) -> HashMap { + let resp = self.get_versions_from_hashes(hashes, algorithm, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn update_individual_files( + &self, + algorithm: &str, + hashes: Vec, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::post() + .uri("/v2/version_files/update_individual") + .append_header(("Authorization", pat)) + .set_json(json!({ + "algorithm": algorithm, + "hashes": hashes + })) + .to_request(); + self.call(req).await + } + + pub async fn update_individual_files_deserialized( + &self, + algorithm: &str, + hashes: Vec, + pat: &str, + ) -> HashMap { + let resp = self.update_individual_files(algorithm, hashes, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } +} + +#[async_trait(?Send)] +impl ApiVersion for ApiV2 { + async fn add_public_version( + &self, + project_id: ProjectId, + version_number: &str, + version_jar: TestFile, + ordering: Option, + modify_json: Option, + pat: &str, + ) -> ServiceResponse { + let creation_data = get_public_version_creation_data( + project_id, + version_number, + version_jar, + ordering, + modify_json, + ); + // Add a project. let req = TestRequest::post() .uri("/v2/version") .append_header(("Authorization", pat)) .set_multipart(creation_data.segment_data) .to_request(); - let resp = self.call(req).await; - assert_status(&resp, StatusCode::OK); - let value: serde_json::Value = test::read_body_json(resp).await; - let version_id = value["id"].as_str().unwrap(); - - // // Approve as a moderator. - // let req = TestRequest::patch() - // .uri(&format!("/v2/project/{}", creation_data.slug)) - // .append_header(("Authorization", MOD_USER_PAT)) - // .set_json(json!( - // { - // "status": "approved" - // } - // )) - // .to_request(); - // let resp = self.call(req).await; - // assert_status(resp, StatusCode::NO_CONTENT); + self.call(req).await + } - self.get_version_deserialized(version_id, pat).await + async fn add_public_version_deserialized_common( + &self, + project_id: ProjectId, + version_number: &str, + version_jar: TestFile, + ordering: Option, + modify_json: Option, + pat: &str, + ) -> CommonVersion { + let resp = self + .add_public_version( + project_id, + version_number, + version_jar, + ordering, + modify_json, + pat, + ) + .await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await } - pub async fn get_version(&self, id: &str, pat: &str) -> ServiceResponse { + async fn get_version(&self, id: &str, pat: &str) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v2/version/{id}")) .append_header(("Authorization", pat)) @@ -62,13 +144,13 @@ impl ApiV2 { self.call(req).await } - pub async fn get_version_deserialized(&self, id: &str, pat: &str) -> LegacyVersion { + async fn get_version_deserialized_common(&self, id: &str, pat: &str) -> CommonVersion { let resp = self.get_version(id, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn edit_version( + async fn edit_version( &self, version_id: &str, patch: serde_json::Value, @@ -83,7 +165,7 @@ impl ApiV2 { self.call(req).await } - pub async fn get_version_from_hash( + async fn get_version_from_hash( &self, hash: &str, algorithm: &str, @@ -96,18 +178,18 @@ impl ApiV2 { self.call(req).await } - pub async fn get_version_from_hash_deserialized( + async fn get_version_from_hash_deserialized_common( &self, hash: &str, algorithm: &str, pat: &str, - ) -> LegacyVersion { + ) -> CommonVersion { let resp = self.get_version_from_hash(hash, algorithm, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_versions_from_hashes( + async fn get_versions_from_hashes( &self, hashes: &[&str], algorithm: &str, @@ -124,18 +206,18 @@ impl ApiV2 { self.call(req).await } - pub async fn get_versions_from_hashes_deserialized( + async fn get_versions_from_hashes_deserialized_common( &self, hashes: &[&str], algorithm: &str, pat: &str, - ) -> HashMap { + ) -> HashMap { let resp = self.get_versions_from_hashes(hashes, algorithm, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_update_from_hash( + async fn get_update_from_hash( &self, hash: &str, algorithm: &str, @@ -158,7 +240,7 @@ impl ApiV2 { self.call(req).await } - pub async fn get_update_from_hash_deserialized( + async fn get_update_from_hash_deserialized_common( &self, hash: &str, algorithm: &str, @@ -166,7 +248,7 @@ impl ApiV2 { game_versions: Option>, version_types: Option>, pat: &str, - ) -> LegacyVersion { + ) -> CommonVersion { let resp = self .get_update_from_hash(hash, algorithm, loaders, game_versions, version_types, pat) .await; @@ -174,7 +256,7 @@ impl ApiV2 { test::read_body_json(resp).await } - pub async fn update_files( + async fn update_files( &self, algorithm: &str, hashes: Vec, @@ -197,7 +279,7 @@ impl ApiV2 { self.call(req).await } - pub async fn update_files_deserialized( + async fn update_files_deserialized_common( &self, algorithm: &str, hashes: Vec, @@ -205,7 +287,7 @@ impl ApiV2 { game_versions: Option>, version_types: Option>, pat: &str, - ) -> HashMap { + ) -> HashMap { let resp = self .update_files( algorithm, @@ -220,37 +302,9 @@ impl ApiV2 { test::read_body_json(resp).await } - pub async fn update_individual_files( - &self, - algorithm: &str, - hashes: Vec, - pat: &str, - ) -> ServiceResponse { - let req = test::TestRequest::post() - .uri("/v2/version_files/update_individual") - .append_header(("Authorization", pat)) - .set_json(json!({ - "algorithm": algorithm, - "hashes": hashes - })) - .to_request(); - self.call(req).await - } - - pub async fn update_individual_files_deserialized( - &self, - algorithm: &str, - hashes: Vec, - pat: &str, - ) -> HashMap { - let resp = self.update_individual_files(algorithm, hashes, pat).await; - assert_eq!(resp.status(), 200); - test::read_body_json(resp).await - } - // TODO: Not all fields are tested currently in the V2 tests, only the v2-v3 relevant ones are #[allow(clippy::too_many_arguments)] - pub async fn get_project_versions( + async fn get_project_versions( &self, project_id_slug: &str, game_versions: Option>, @@ -300,7 +354,7 @@ impl ApiV2 { } #[allow(clippy::too_many_arguments)] - pub async fn get_project_versions_deserialized( + async fn get_project_versions_deserialized_common( &self, slug: &str, game_versions: Option>, @@ -310,7 +364,7 @@ impl ApiV2 { limit: Option, offset: Option, pat: &str, - ) -> Vec { + ) -> Vec { let resp = self .get_project_versions( slug, @@ -327,80 +381,40 @@ impl ApiV2 { test::read_body_json(resp).await } - // TODO: remove redundancy in these functions- some are essentially repeats - pub async fn create_default_version( + async fn edit_version_ordering( &self, - project_id: &str, + version_id: &str, ordering: Option, pat: &str, - ) -> LegacyVersion { - let json_data = json!( + ) -> ServiceResponse { + let request = test::TestRequest::patch() + .uri(&format!("/v2/version/{version_id}")) + .set_json(json!( { - "project_id": project_id, - "file_parts": ["basic-mod-different.jar"], - "version_number": "1.2.3.4", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "release_channel": "release", - "loaders": ["fabric"], - "featured": true, - "ordering": ordering, + "ordering": ordering } - ); - let json_segment = labrinth::util::actix::MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: labrinth::util::actix::MultipartSegmentData::Text( - serde_json::to_string(&json_data).unwrap(), - ), - }; - let file_segment = labrinth::util::actix::MultipartSegment { - name: "basic-mod-different.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: labrinth::util::actix::MultipartSegmentData::Binary( - include_bytes!("../../../tests/files/basic-mod-different.jar").to_vec(), - ), - }; - - let request = test::TestRequest::post() - .uri("/v2/version") - .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + )) .append_header((AUTHORIZATION, pat)) .to_request(); - let resp = self.call(request).await; - assert_status(&resp, StatusCode::OK); - test::read_body_json(resp).await + self.call(request).await } - pub async fn get_versions(&self, version_ids: Vec, pat: &str) -> Vec { + async fn get_versions(&self, version_ids: Vec, pat: &str) -> ServiceResponse { let ids = url_encode_json_serialized_vec(&version_ids); let request = test::TestRequest::get() .uri(&format!("/v2/versions?ids={}", ids)) .append_header((AUTHORIZATION, pat)) .to_request(); - let resp = self.call(request).await; - assert_status(&resp, StatusCode::OK); - test::read_body_json(resp).await + self.call(request).await } - pub async fn edit_version_ordering( + async fn get_versions_deserialized_common( &self, - version_id: &str, - ordering: Option, + version_ids: Vec, pat: &str, - ) -> ServiceResponse { - let request = test::TestRequest::patch() - .uri(&format!("/v2/version/{version_id}")) - .set_json(json!( - { - "ordering": ordering - } - )) - .append_header((AUTHORIZATION, pat)) - .to_request(); - self.call(request).await + ) -> Vec { + let resp = self.get_versions(version_ids, pat).await; + assert_status(&resp, StatusCode::OK); + test::read_body_json(resp).await } } diff --git a/tests/common/api_v3/mod.rs b/tests/common/api_v3/mod.rs index 9ed4bce2..6f1fa953 100644 --- a/tests/common/api_v3/mod.rs +++ b/tests/common/api_v3/mod.rs @@ -1,7 +1,12 @@ #![allow(dead_code)] -use super::environment::LocalService; -use actix_web::dev::ServiceResponse; +use super::{ + api_common::{Api, ApiBuildable}, + environment::LocalService, +}; +use actix_web::{dev::ServiceResponse, test, App}; +use async_trait::async_trait; +use labrinth::LabrinthConfig; use std::rc::Rc; pub mod oauth; @@ -18,12 +23,23 @@ pub struct ApiV3 { pub test_app: Rc, } -impl ApiV3 { - pub async fn call(&self, req: actix_http::Request) -> ServiceResponse { +#[async_trait(?Send)] +impl ApiBuildable for ApiV3 { + async fn build(labrinth_config: LabrinthConfig) -> Self { + let app = App::new().configure(|cfg| labrinth::app_config(cfg, labrinth_config.clone())); + let test_app: Rc = Rc::new(test::init_service(app).await); + + Self { test_app } + } +} + +#[async_trait(?Send)] +impl Api for ApiV3 { + async fn call(&self, req: actix_http::Request) -> ServiceResponse { self.test_app.call(req).await.unwrap() } - pub async fn reset_search_index(&self) -> ServiceResponse { + async fn reset_search_index(&self) -> ServiceResponse { let req = actix_web::test::TestRequest::post() .uri("/v3/admin/_force_reindex") .append_header(( diff --git a/tests/common/api_v3/oauth.rs b/tests/common/api_v3/oauth.rs index 125b3dec..4e6c7669 100644 --- a/tests/common/api_v3/oauth.rs +++ b/tests/common/api_v3/oauth.rs @@ -10,7 +10,7 @@ use labrinth::auth::oauth::{ }; use reqwest::header::{AUTHORIZATION, LOCATION}; -use crate::common::asserts::assert_status; +use crate::common::{api_common::Api, asserts::assert_status}; use super::ApiV3; diff --git a/tests/common/api_v3/oauth_clients.rs b/tests/common/api_v3/oauth_clients.rs index a08ad1d6..b5c3ef56 100644 --- a/tests/common/api_v3/oauth_clients.rs +++ b/tests/common/api_v3/oauth_clients.rs @@ -13,7 +13,7 @@ use labrinth::{ use reqwest::header::AUTHORIZATION; use serde_json::json; -use crate::common::asserts::assert_status; +use crate::common::{api_common::Api, asserts::assert_status}; use super::ApiV3; diff --git a/tests/common/api_v3/organization.rs b/tests/common/api_v3/organization.rs index 4a17cd2f..4e2eb0a6 100644 --- a/tests/common/api_v3/organization.rs +++ b/tests/common/api_v3/organization.rs @@ -6,6 +6,8 @@ use bytes::Bytes; use labrinth::models::{organizations::Organization, v3::projects::Project}; use serde_json::json; +use crate::common::api_common::Api; + use super::{request_data::ImageData, ApiV3}; impl ApiV3 { diff --git a/tests/common/api_v3/project.rs b/tests/common/api_v3/project.rs index 2ffc1d7a..a2616c09 100644 --- a/tests/common/api_v3/project.rs +++ b/tests/common/api_v3/project.rs @@ -5,29 +5,36 @@ use actix_web::{ dev::ServiceResponse, test::{self, TestRequest}, }; +use async_trait::async_trait; use bytes::Bytes; use chrono::{DateTime, Utc}; -use labrinth::{ - models::v3::projects::{Project, Version}, - search::SearchResults, - util::actix::AppendsMultipart, -}; +use labrinth::{search::SearchResults, util::actix::AppendsMultipart}; use rust_decimal::Decimal; use serde_json::json; -use crate::common::{asserts::assert_status, database::MOD_USER_PAT}; - -use super::{ - request_data::{ImageData, ProjectCreationRequestData}, - ApiV3, +use crate::common::{ + api_common::{ + models::{CommonImageData, CommonProject, CommonVersion}, + Api, ApiProject, + }, + asserts::assert_status, + database::MOD_USER_PAT, + dummy_data::TestFile, }; -impl ApiV3 { - pub async fn add_public_project( +use super::{request_data::get_public_project_creation_data, ApiV3}; + +#[async_trait(?Send)] +impl ApiProject for ApiV3 { + async fn add_public_project( &self, - creation_data: ProjectCreationRequestData, + slug: &str, + version_jar: Option, + modify_json: Option, pat: &str, - ) -> (Project, Vec) { + ) -> (CommonProject, Vec) { + let creation_data = get_public_project_creation_data(slug, version_jar, modify_json); + // Add a project. let req = TestRequest::post() .uri("/v3/project") @@ -50,9 +57,8 @@ impl ApiV3 { let resp = self.call(req).await; assert_status(&resp, StatusCode::NO_CONTENT); - let project = self - .get_project_deserialized(&creation_data.slug, pat) - .await; + let project = self.get_project(&creation_data.slug, pat).await; + let project = test::read_body_json(project).await; // Get project's versions let req = TestRequest::get() @@ -60,12 +66,12 @@ impl ApiV3 { .append_header(("Authorization", pat)) .to_request(); let resp = self.call(req).await; - let versions: Vec = test::read_body_json(resp).await; + let versions: Vec = test::read_body_json(resp).await; (project, versions) } - pub async fn remove_project(&self, project_slug_or_id: &str, pat: &str) -> ServiceResponse { + async fn remove_project(&self, project_slug_or_id: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v3/project/{project_slug_or_id}")) .append_header(("Authorization", pat)) @@ -75,20 +81,21 @@ impl ApiV3 { resp } - pub async fn get_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse { + async fn get_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v3/project/{id_or_slug}")) .append_header(("Authorization", pat)) .to_request(); self.call(req).await } - pub async fn get_project_deserialized(&self, id_or_slug: &str, pat: &str) -> Project { + + async fn get_project_deserialized_common(&self, id_or_slug: &str, pat: &str) -> CommonProject { let resp = self.get_project(id_or_slug, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_user_projects(&self, user_id_or_username: &str, pat: &str) -> ServiceResponse { + async fn get_user_projects(&self, user_id_or_username: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/user/{}/projects", user_id_or_username)) .append_header(("Authorization", pat)) @@ -96,17 +103,17 @@ impl ApiV3 { self.call(req).await } - pub async fn get_user_projects_deserialized( + async fn get_user_projects_deserialized_common( &self, user_id_or_username: &str, pat: &str, - ) -> Vec { + ) -> Vec { let resp = self.get_user_projects(user_id_or_username, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn edit_project( + async fn edit_project( &self, id_or_slug: &str, patch: serde_json::Value, @@ -121,14 +128,14 @@ impl ApiV3 { self.call(req).await } - pub async fn edit_project_bulk( + async fn edit_project_bulk( &self, - ids_or_slugs: impl IntoIterator, + ids_or_slugs: &[&str], patch: serde_json::Value, pat: &str, ) -> ServiceResponse { let projects_str = ids_or_slugs - .into_iter() + .iter() .map(|s| format!("\"{}\"", s)) .collect::>() .join(","); @@ -144,10 +151,10 @@ impl ApiV3 { self.call(req).await } - pub async fn edit_project_icon( + async fn edit_project_icon( &self, id_or_slug: &str, - icon: Option, + icon: Option, pat: &str, ) -> ServiceResponse { if let Some(icon) = icon { @@ -173,7 +180,7 @@ impl ApiV3 { } } - pub async fn search_deserialized( + async fn search_deserialized_common( &self, query: Option<&str>, facets: Option, @@ -200,7 +207,9 @@ impl ApiV3 { assert_eq!(status, 200); test::read_body_json(resp).await } +} +impl ApiV3 { pub async fn get_analytics_revenue( &self, id_or_slugs: Vec<&str>, diff --git a/tests/common/api_v3/request_data.rs b/tests/common/api_v3/request_data.rs index 6091992c..3110095b 100644 --- a/tests/common/api_v3/request_data.rs +++ b/tests/common/api_v3/request_data.rs @@ -28,8 +28,12 @@ pub struct ImageData { pub fn get_public_project_creation_data( slug: &str, version_jar: Option, + modify_json: Option, ) -> ProjectCreationRequestData { - let json_data = get_public_project_creation_data_json(slug, version_jar.as_ref()); + let mut json_data = get_public_project_creation_data_json(slug, version_jar.as_ref()); + if let Some(modify_json) = modify_json { + json_patch::patch(&mut json_data, &modify_json).unwrap(); + } let multipart_data = get_public_creation_data_multipart(&json_data, version_jar.as_ref()); ProjectCreationRequestData { slug: slug.to_string(), @@ -42,14 +46,16 @@ pub fn get_public_version_creation_data( project_id: ProjectId, version_number: &str, version_jar: TestFile, + ordering: Option, // closure that takes in a &mut serde_json::Value // and modifies it before it is serialized and sent - modify_json: Option, + modify_json: Option, ) -> VersionCreationRequestData { - let mut json_data = get_public_version_creation_data_json(version_number, &version_jar); + let mut json_data = + get_public_version_creation_data_json(version_number, ordering, &version_jar); json_data["project_id"] = json!(project_id); if let Some(modify_json) = modify_json { - modify_json(&mut json_data); + json_patch::patch(&mut json_data, &modify_json).unwrap(); } let multipart_data = get_public_creation_data_multipart(&json_data, Some(&version_jar)); @@ -62,6 +68,7 @@ pub fn get_public_version_creation_data( pub fn get_public_version_creation_data_json( version_number: &str, + ordering: Option, version_jar: &TestFile, ) -> serde_json::Value { let is_modpack = version_jar.project_type() == "modpack"; @@ -82,6 +89,9 @@ pub fn get_public_version_creation_data_json( if is_modpack { j["mrpack_loaders"] = json!(["fabric"]); } + if let Some(ordering) = ordering { + j["ordering"] = json!(ordering); + } j } @@ -90,7 +100,7 @@ pub fn get_public_project_creation_data_json( version_jar: Option<&TestFile>, ) -> serde_json::Value { let initial_versions = if let Some(jar) = version_jar { - json!([get_public_version_creation_data_json("1.2.3", jar)]) + json!([get_public_version_creation_data_json("1.2.3", None, jar)]) } else { json!([]) }; diff --git a/tests/common/api_v3/tags.rs b/tests/common/api_v3/tags.rs index afd7a8ec..da297a77 100644 --- a/tests/common/api_v3/tags.rs +++ b/tests/common/api_v3/tags.rs @@ -2,18 +2,23 @@ use actix_web::{ dev::ServiceResponse, test::{self, TestRequest}, }; +use async_trait::async_trait; +use labrinth::database::models::loader_fields::LoaderFieldEnumValue; use labrinth::routes::v3::tags::GameData; -use labrinth::{ - database::models::loader_fields::LoaderFieldEnumValue, - routes::v3::tags::{CategoryData, LoaderData}, -}; -use crate::common::database::ADMIN_USER_PAT; +use crate::common::{ + api_common::{ + models::{CommonCategoryData, CommonLoaderData}, + Api, ApiTags, + }, + database::ADMIN_USER_PAT, +}; use super::ApiV3; -impl ApiV3 { - pub async fn get_loaders(&self) -> ServiceResponse { +#[async_trait(?Send)] +impl ApiTags for ApiV3 { + async fn get_loaders(&self) -> ServiceResponse { let req = TestRequest::get() .uri("/v3/tag/loader") .append_header(("Authorization", ADMIN_USER_PAT)) @@ -21,13 +26,13 @@ impl ApiV3 { self.call(req).await } - pub async fn get_loaders_deserialized(&self) -> Vec { + async fn get_loaders_deserialized_common(&self) -> Vec { let resp = self.get_loaders().await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_categories(&self) -> ServiceResponse { + async fn get_categories(&self) -> ServiceResponse { let req = TestRequest::get() .uri("/v3/tag/category") .append_header(("Authorization", ADMIN_USER_PAT)) @@ -35,12 +40,14 @@ impl ApiV3 { self.call(req).await } - pub async fn get_categories_deserialized(&self) -> Vec { + async fn get_categories_deserialized_common(&self) -> Vec { let resp = self.get_categories().await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } +} +impl ApiV3 { pub async fn get_loader_field_variants(&self, loader_field: &str) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v3/loader_field?loader_field={}", loader_field)) @@ -59,7 +66,7 @@ impl ApiV3 { } // TODO: fold this into v3 API of other v3 testing PR - pub async fn get_games(&self) -> ServiceResponse { + async fn get_games(&self) -> ServiceResponse { let req = TestRequest::get() .uri("/v3/games") .append_header(("Authorization", ADMIN_USER_PAT)) diff --git a/tests/common/api_v3/team.rs b/tests/common/api_v3/team.rs index 05b6b137..f32772aa 100644 --- a/tests/common/api_v3/team.rs +++ b/tests/common/api_v3/team.rs @@ -1,17 +1,22 @@ use actix_http::StatusCode; use actix_web::{dev::ServiceResponse, test}; -use labrinth::models::{ - notifications::Notification, - teams::{OrganizationPermissions, ProjectPermissions, TeamMember}, -}; +use async_trait::async_trait; +use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; use serde_json::json; -use crate::common::asserts::assert_status; +use crate::common::{ + api_common::{ + models::{CommonNotification, CommonTeamMember}, + Api, ApiTeams, + }, + asserts::assert_status, +}; use super::ApiV3; -impl ApiV3 { - pub async fn get_team_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { +#[async_trait(?Send)] +impl ApiTeams for ApiV3 { + async fn get_team_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/team/{id_or_title}/members")) .append_header(("Authorization", pat)) @@ -19,17 +24,17 @@ impl ApiV3 { self.call(req).await } - pub async fn get_team_members_deserialized( + async fn get_team_members_deserialized_common( &self, id_or_title: &str, pat: &str, - ) -> Vec { + ) -> Vec { let resp = self.get_team_members(id_or_title, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_project_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + async fn get_project_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/project/{id_or_title}/members")) .append_header(("Authorization", pat)) @@ -37,17 +42,17 @@ impl ApiV3 { self.call(req).await } - pub async fn get_project_members_deserialized( + async fn get_project_members_deserialized_common( &self, id_or_title: &str, pat: &str, - ) -> Vec { + ) -> Vec { let resp = self.get_project_members(id_or_title, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_organization_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + async fn get_organization_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/organization/{id_or_title}/members")) .append_header(("Authorization", pat)) @@ -55,17 +60,17 @@ impl ApiV3 { self.call(req).await } - pub async fn get_organization_members_deserialized( + async fn get_organization_members_deserialized_common( &self, id_or_title: &str, pat: &str, - ) -> Vec { + ) -> Vec { let resp = self.get_organization_members(id_or_title, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn join_team(&self, team_id: &str, pat: &str) -> ServiceResponse { + async fn join_team(&self, team_id: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::post() .uri(&format!("/v3/team/{team_id}/join")) .append_header(("Authorization", pat)) @@ -73,12 +78,7 @@ impl ApiV3 { self.call(req).await } - pub async fn remove_from_team( - &self, - team_id: &str, - user_id: &str, - pat: &str, - ) -> ServiceResponse { + async fn remove_from_team(&self, team_id: &str, user_id: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v3/team/{team_id}/members/{user_id}")) .append_header(("Authorization", pat)) @@ -86,7 +86,7 @@ impl ApiV3 { self.call(req).await } - pub async fn edit_team_member( + async fn edit_team_member( &self, team_id: &str, user_id: &str, @@ -101,7 +101,7 @@ impl ApiV3 { self.call(req).await } - pub async fn transfer_team_ownership( + async fn transfer_team_ownership( &self, team_id: &str, user_id: &str, @@ -117,7 +117,7 @@ impl ApiV3 { self.call(req).await } - pub async fn get_user_notifications(&self, user_id: &str, pat: &str) -> ServiceResponse { + async fn get_user_notifications(&self, user_id: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::get() .uri(&format!("/v3/user/{user_id}/notifications")) .append_header(("Authorization", pat)) @@ -125,28 +125,24 @@ impl ApiV3 { self.call(req).await } - pub async fn get_user_notifications_deserialized( + async fn get_user_notifications_deserialized_common( &self, user_id: &str, pat: &str, - ) -> Vec { + ) -> Vec { let resp = self.get_user_notifications(user_id, pat).await; assert_status(&resp, StatusCode::OK); test::read_body_json(resp).await } - pub async fn mark_notification_read( - &self, - notification_id: &str, - pat: &str, - ) -> ServiceResponse { + async fn mark_notification_read(&self, notification_id: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::patch() .uri(&format!("/v3/notification/{notification_id}")) .append_header(("Authorization", pat)) .to_request(); self.call(req).await } - pub async fn add_user_to_team( + async fn add_user_to_team( &self, team_id: &str, user_id: &str, @@ -166,7 +162,7 @@ impl ApiV3 { self.call(req).await } - pub async fn delete_notification(&self, notification_id: &str, pat: &str) -> ServiceResponse { + async fn delete_notification(&self, notification_id: &str, pat: &str) -> ServiceResponse { let req = test::TestRequest::delete() .uri(&format!("/v3/notification/{notification_id}")) .append_header(("Authorization", pat)) diff --git a/tests/common/api_v3/version.rs b/tests/common/api_v3/version.rs index b1ae5693..5d2463d3 100644 --- a/tests/common/api_v3/version.rs +++ b/tests/common/api_v3/version.rs @@ -1,33 +1,115 @@ use std::collections::HashMap; +use super::{request_data::get_public_version_creation_data, ApiV3}; +use crate::common::{ + api_common::{models::CommonVersion, Api, ApiVersion}, + asserts::assert_status, + dummy_data::TestFile, +}; use actix_http::{header::AUTHORIZATION, StatusCode}; use actix_web::{ dev::ServiceResponse, test::{self, TestRequest}, }; +use async_trait::async_trait; use labrinth::{ - models::{projects::VersionType, v3::projects::Version}, + models::{ + projects::{ProjectId, VersionType}, + v3::projects::Version, + }, routes::v3::version_file::FileUpdateData, util::actix::AppendsMultipart, }; use serde_json::json; -use crate::common::asserts::assert_status; - -use super::{request_data::VersionCreationRequestData, ApiV3}; - pub fn url_encode_json_serialized_vec(elements: &[String]) -> String { let serialized = serde_json::to_string(&elements).unwrap(); urlencoding::encode(&serialized).to_string() } impl ApiV3 { - pub async fn add_public_version( + pub async fn add_public_version_deserialized( + &self, + project_id: ProjectId, + version_number: &str, + version_jar: TestFile, + ordering: Option, + modify_json: Option, + pat: &str, + ) -> Version { + let resp = self + .add_public_version( + project_id, + version_number, + version_jar, + ordering, + modify_json, + pat, + ) + .await; + assert_status(&resp, StatusCode::OK); + let value: serde_json::Value = test::read_body_json(resp).await; + let version_id = value["id"].as_str().unwrap(); + let version = self.get_version(version_id, pat).await; + assert_status(&version, StatusCode::OK); + test::read_body_json(version).await + } + + pub async fn get_version_deserialized(&self, id: &str, pat: &str) -> Version { + let resp = self.get_version(id, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn update_individual_files( + &self, + algorithm: &str, + hashes: Vec, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::post() + .uri("/v3/version_files/update_individual") + .append_header(("Authorization", pat)) + .set_json(json!({ + "algorithm": algorithm, + "hashes": hashes + })) + .to_request(); + self.call(req).await + } + + pub async fn update_individual_files_deserialized( &self, - creation_data: VersionCreationRequestData, + algorithm: &str, + hashes: Vec, + pat: &str, + ) -> HashMap { + let resp = self.update_individual_files(algorithm, hashes, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } +} + +#[async_trait(?Send)] +impl ApiVersion for ApiV3 { + async fn add_public_version( + &self, + project_id: ProjectId, + version_number: &str, + version_jar: TestFile, + ordering: Option, + modify_json: Option, pat: &str, ) -> ServiceResponse { - // Add a project. + let creation_data = get_public_version_creation_data( + project_id, + version_number, + version_jar, + ordering, + modify_json, + ); + + // Add a versiom. let req = TestRequest::post() .uri("/v3/version") .append_header(("Authorization", pat)) @@ -36,19 +118,30 @@ impl ApiV3 { self.call(req).await } - pub async fn add_public_version_deserialized( + async fn add_public_version_deserialized_common( &self, - creation_data: VersionCreationRequestData, + project_id: ProjectId, + version_number: &str, + version_jar: TestFile, + ordering: Option, + modify_json: Option, pat: &str, - ) -> Version { - let resp = self.add_public_version(creation_data, pat).await; + ) -> CommonVersion { + let resp = self + .add_public_version( + project_id, + version_number, + version_jar, + ordering, + modify_json, + pat, + ) + .await; assert_status(&resp, StatusCode::OK); - let value: serde_json::Value = test::read_body_json(resp).await; - let version_id = value["id"].as_str().unwrap(); - self.get_version_deserialized(version_id, pat).await + test::read_body_json(resp).await } - pub async fn get_version(&self, id: &str, pat: &str) -> ServiceResponse { + async fn get_version(&self, id: &str, pat: &str) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v3/version/{id}")) .append_header(("Authorization", pat)) @@ -56,13 +149,13 @@ impl ApiV3 { self.call(req).await } - pub async fn get_version_deserialized(&self, id: &str, pat: &str) -> Version { + async fn get_version_deserialized_common(&self, id: &str, pat: &str) -> CommonVersion { let resp = self.get_version(id, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn edit_version( + async fn edit_version( &self, version_id: &str, patch: serde_json::Value, @@ -77,7 +170,7 @@ impl ApiV3 { self.call(req).await } - pub async fn get_version_from_hash( + async fn get_version_from_hash( &self, hash: &str, algorithm: &str, @@ -90,18 +183,18 @@ impl ApiV3 { self.call(req).await } - pub async fn get_version_from_hash_deserialized( + async fn get_version_from_hash_deserialized_common( &self, hash: &str, algorithm: &str, pat: &str, - ) -> Version { + ) -> CommonVersion { let resp = self.get_version_from_hash(hash, algorithm, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_versions_from_hashes( + async fn get_versions_from_hashes( &self, hashes: &[&str], algorithm: &str, @@ -118,18 +211,18 @@ impl ApiV3 { self.call(req).await } - pub async fn get_versions_from_hashes_deserialized( + async fn get_versions_from_hashes_deserialized_common( &self, hashes: &[&str], algorithm: &str, pat: &str, - ) -> HashMap { + ) -> HashMap { let resp = self.get_versions_from_hashes(hashes, algorithm, pat).await; assert_eq!(resp.status(), 200); test::read_body_json(resp).await } - pub async fn get_update_from_hash( + async fn get_update_from_hash( &self, hash: &str, algorithm: &str, @@ -161,7 +254,7 @@ impl ApiV3 { self.call(req).await } - pub async fn get_update_from_hash_deserialized( + async fn get_update_from_hash_deserialized_common( &self, hash: &str, algorithm: &str, @@ -169,7 +262,7 @@ impl ApiV3 { game_versions: Option>, version_types: Option>, pat: &str, - ) -> Version { + ) -> CommonVersion { let resp = self .get_update_from_hash(hash, algorithm, loaders, game_versions, version_types, pat) .await; @@ -177,7 +270,7 @@ impl ApiV3 { test::read_body_json(resp).await } - pub async fn update_files( + async fn update_files( &self, algorithm: &str, hashes: Vec, @@ -210,7 +303,7 @@ impl ApiV3 { self.call(req).await } - pub async fn update_files_deserialized( + async fn update_files_deserialized_common( &self, algorithm: &str, hashes: Vec, @@ -218,7 +311,7 @@ impl ApiV3 { game_versions: Option>, version_types: Option>, pat: &str, - ) -> HashMap { + ) -> HashMap { let resp = self .update_files( algorithm, @@ -233,37 +326,9 @@ impl ApiV3 { test::read_body_json(resp).await } - pub async fn update_individual_files( - &self, - algorithm: &str, - hashes: Vec, - pat: &str, - ) -> ServiceResponse { - let req = test::TestRequest::post() - .uri("/v3/version_files/update_individual") - .append_header(("Authorization", pat)) - .set_json(json!({ - "algorithm": algorithm, - "hashes": hashes - })) - .to_request(); - self.call(req).await - } - - pub async fn update_individual_files_deserialized( - &self, - algorithm: &str, - hashes: Vec, - pat: &str, - ) -> HashMap { - let resp = self.update_individual_files(algorithm, hashes, pat).await; - assert_eq!(resp.status(), 200); - test::read_body_json(resp).await - } - // TODO: Not all fields are tested currently in the v3 tests, only the v2-v3 relevant ones are #[allow(clippy::too_many_arguments)] - pub async fn get_project_versions( + async fn get_project_versions( &self, project_id_slug: &str, game_versions: Option>, @@ -313,7 +378,7 @@ impl ApiV3 { } #[allow(clippy::too_many_arguments)] - pub async fn get_project_versions_deserialized( + async fn get_project_versions_deserialized_common( &self, slug: &str, game_versions: Option>, @@ -323,7 +388,7 @@ impl ApiV3 { limit: Option, offset: Option, pat: &str, - ) -> Vec { + ) -> Vec { let resp = self .get_project_versions( slug, @@ -341,82 +406,40 @@ impl ApiV3 { } // TODO: remove redundancy in these functions - - pub async fn create_default_version( + async fn edit_version_ordering( &self, - project_id: &str, + version_id: &str, ordering: Option, pat: &str, - ) -> Version { - let json_data = json!( + ) -> ServiceResponse { + let request = test::TestRequest::patch() + .uri(&format!("/v3/version/{version_id}")) + .set_json(json!( { - "project_id": project_id, - "file_parts": ["basic-mod-different.jar"], - "version_number": "1.2.3.4", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "client_side": "required", - "server_side": "optional", - "release_channel": "release", - "loaders": ["fabric"], - "featured": true, - "ordering": ordering, + "ordering": ordering } - ); - let json_segment = labrinth::util::actix::MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: labrinth::util::actix::MultipartSegmentData::Text( - serde_json::to_string(&json_data).unwrap(), - ), - }; - let file_segment = labrinth::util::actix::MultipartSegment { - name: "basic-mod-different.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: labrinth::util::actix::MultipartSegmentData::Binary( - include_bytes!("../../../tests/files/basic-mod-different.jar").to_vec(), - ), - }; - - let request = test::TestRequest::post() - .uri("/v3/version") - .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + )) .append_header((AUTHORIZATION, pat)) .to_request(); - let resp = self.call(request).await; - assert_status(&resp, StatusCode::OK); - test::read_body_json(resp).await + self.call(request).await } - pub async fn get_versions(&self, version_ids: Vec, pat: &str) -> Vec { + async fn get_versions(&self, version_ids: Vec, pat: &str) -> ServiceResponse { let ids = url_encode_json_serialized_vec(&version_ids); let request = test::TestRequest::get() .uri(&format!("/v3/versions?ids={}", ids)) .append_header((AUTHORIZATION, pat)) .to_request(); - let resp = self.call(request).await; - assert_status(&resp, StatusCode::OK); - test::read_body_json(resp).await + self.call(request).await } - pub async fn edit_version_ordering( + async fn get_versions_deserialized_common( &self, - version_id: &str, - ordering: Option, + version_ids: Vec, pat: &str, - ) -> ServiceResponse { - let request = test::TestRequest::patch() - .uri(&format!("/v3/version/{version_id}")) - .set_json(json!( - { - "ordering": ordering - } - )) - .append_header((AUTHORIZATION, pat)) - .to_request(); - self.call(request).await + ) -> Vec { + let resp = self.get_versions(version_ids, pat).await; + assert_status(&resp, StatusCode::OK); + test::read_body_json(resp).await } } diff --git a/tests/common/asserts.rs b/tests/common/asserts.rs index cc6e35a6..5a1be6d8 100644 --- a/tests/common/asserts.rs +++ b/tests/common/asserts.rs @@ -4,6 +4,8 @@ use crate::common::get_json_val_str; use itertools::Itertools; use labrinth::models::v3::projects::Version; +use super::api_common::models::CommonVersion; + pub fn assert_status(response: &actix_web::dev::ServiceResponse, status: actix_http::StatusCode) { assert_eq!(response.status(), status, "{:#?}", response.response()); } @@ -16,6 +18,14 @@ pub fn assert_version_ids(versions: &[Version], expected_ids: Vec) { assert_eq!(version_ids, expected_ids); } +pub fn assert_common_version_ids(versions: &[CommonVersion], expected_ids: Vec) { + let version_ids = versions + .iter() + .map(|v| get_json_val_str(v.id)) + .collect_vec(); + assert_eq!(version_ids, expected_ids); +} + pub fn assert_any_status_except( response: &actix_web::dev::ServiceResponse, status: actix_http::StatusCode, diff --git a/tests/common/database.rs b/tests/common/database.rs index 988cc028..d3092929 100644 --- a/tests/common/database.rs +++ b/tests/common/database.rs @@ -7,7 +7,7 @@ use url::Url; use crate::common::{dummy_data, environment::TestEnvironment}; -use super::dummy_data::DUMMY_DATA_UPDATE; +use super::{api_v3::ApiV3, dummy_data::DUMMY_DATA_UPDATE}; // The dummy test database adds a fair bit of 'dummy' data to test with. // Some constants are used to refer to that data, and are described here. @@ -168,13 +168,20 @@ impl TemporaryDatabase { if !dummy_data_exists { // Add dummy data - let temporary_test_env = TestEnvironment::build_with_db(TemporaryDatabase { - pool: pool.clone(), - database_name: TEMPLATE_DATABASE_NAME.to_string(), - redis_pool: RedisPool::new(Some(generate_random_name("test_template_"))), - }) + let temporary_test_env = + TestEnvironment::::build_with_db(TemporaryDatabase { + pool: pool.clone(), + database_name: TEMPLATE_DATABASE_NAME.to_string(), + redis_pool: RedisPool::new(Some(generate_random_name( + "test_template_", + ))), + }) + .await; + dummy_data::add_dummy_data( + &temporary_test_env.setup_api, + temporary_test_env.db.clone(), + ) .await; - dummy_data::add_dummy_data(&temporary_test_env).await; temporary_test_env.db.pool.close().await; } pool.close().await; diff --git a/tests/common/dummy_data.rs b/tests/common/dummy_data.rs index aef26a71..1d4988a4 100644 --- a/tests/common/dummy_data.rs +++ b/tests/common/dummy_data.rs @@ -4,19 +4,23 @@ use std::io::{Cursor, Write}; use actix_http::StatusCode; use actix_web::test::{self, TestRequest}; use labrinth::models::{ - oauth_clients::OAuthClient, - organizations::Organization, - pats::Scopes, - v3::projects::{Project, Version}, + oauth_clients::OAuthClient, organizations::Organization, pats::Scopes, projects::ProjectId, }; use serde_json::json; use sqlx::Executor; use zip::{write::FileOptions, CompressionMethod, ZipWriter}; -use crate::common::database::USER_USER_PAT; +use crate::common::{api_common::Api, database::USER_USER_PAT}; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; -use super::{api_v3::request_data::get_public_project_creation_data, environment::TestEnvironment}; +use super::{ + api_common::{ + models::{CommonProject, CommonVersion}, + ApiProject, + }, + api_v3::ApiV3, + database::TemporaryDatabase, +}; use super::{asserts::assert_status, database::USER_USER_ID, get_json_val_str}; @@ -170,10 +174,10 @@ pub struct DummyData { impl DummyData { pub fn new( - project_alpha: Project, - project_alpha_version: Version, - project_beta: Project, - project_beta_version: Version, + project_alpha: CommonProject, + project_alpha_version: CommonVersion, + project_beta: CommonProject, + project_beta_version: CommonVersion, organization_zeta: Organization, oauth_client_alpha: OAuthClient, ) -> Self { @@ -182,6 +186,7 @@ impl DummyData { team_id: project_alpha.team.to_string(), project_id: project_alpha.id.to_string(), project_slug: project_alpha.slug.unwrap(), + project_id_parsed: project_alpha.id, version_id: project_alpha_version.id.to_string(), thread_id: project_alpha.thread_id.to_string(), file_hash: project_alpha_version.files[0].hashes["sha1"].clone(), @@ -191,6 +196,7 @@ impl DummyData { team_id: project_beta.team.to_string(), project_id: project_beta.id.to_string(), project_slug: project_beta.slug.unwrap(), + project_id_parsed: project_beta.id, version_id: project_beta_version.id.to_string(), thread_id: project_beta.thread_id.to_string(), file_hash: project_beta_version.files[0].hashes["sha1"].clone(), @@ -220,6 +226,7 @@ impl DummyData { pub struct DummyProjectAlpha { pub project_id: String, pub project_slug: String, + pub project_id_parsed: ProjectId, pub version_id: String, pub thread_id: String, pub file_hash: String, @@ -230,6 +237,7 @@ pub struct DummyProjectAlpha { pub struct DummyProjectBeta { pub project_id: String, pub project_slug: String, + pub project_id_parsed: ProjectId, pub version_id: String, pub thread_id: String, pub file_hash: String, @@ -250,9 +258,9 @@ pub struct DummyOAuthClientAlpha { pub valid_redirect_uri: String, } -pub async fn add_dummy_data(test_env: &TestEnvironment) -> DummyData { +pub async fn add_dummy_data(api: &ApiV3, db: TemporaryDatabase) -> DummyData { // Adds basic dummy data to the database directly with sql (user, pats) - let pool = &test_env.db.pool.clone(); + let pool = &db.pool.clone(); pool.execute( include_str!("../files/dummy_data.sql") @@ -262,12 +270,12 @@ pub async fn add_dummy_data(test_env: &TestEnvironment) -> DummyData { .await .unwrap(); - let (alpha_project, alpha_version) = add_project_alpha(test_env).await; - let (beta_project, beta_version) = add_project_beta(test_env).await; + let (alpha_project, alpha_version) = add_project_alpha(api).await; + let (beta_project, beta_version) = add_project_beta(api).await; - let zeta_organization = add_organization_zeta(test_env).await; + let zeta_organization = add_organization_zeta(api).await; - let oauth_client_alpha = get_oauth_client_alpha(test_env).await; + let oauth_client_alpha = get_oauth_client_alpha(api).await; sqlx::query("INSERT INTO dummy_data (update_id) VALUES ($1)") .bind(DUMMY_DATA_UPDATE) @@ -285,13 +293,13 @@ pub async fn add_dummy_data(test_env: &TestEnvironment) -> DummyData { ) } -pub async fn get_dummy_data(test_env: &TestEnvironment) -> DummyData { - let (alpha_project, alpha_version) = get_project_alpha(test_env).await; - let (beta_project, beta_version) = get_project_beta(test_env).await; +pub async fn get_dummy_data(api: &ApiV3) -> DummyData { + let (alpha_project, alpha_version) = get_project_alpha(api).await; + let (beta_project, beta_version) = get_project_beta(api).await; - let zeta_organization = get_organization_zeta(test_env).await; + let zeta_organization = get_organization_zeta(api).await; - let oauth_client_alpha = get_oauth_client_alpha(test_env).await; + let oauth_client_alpha = get_oauth_client_alpha(api).await; DummyData::new( alpha_project, @@ -303,18 +311,19 @@ pub async fn get_dummy_data(test_env: &TestEnvironment) -> DummyData { ) } -pub async fn add_project_alpha(test_env: &TestEnvironment) -> (Project, Version) { - let (project, versions) = test_env - .v3 +pub async fn add_project_alpha(api: &ApiV3) -> (CommonProject, CommonVersion) { + let (project, versions) = api .add_public_project( - get_public_project_creation_data("alpha", Some(TestFile::DummyProjectAlpha)), + "alpha", + Some(TestFile::DummyProjectAlpha), + None, USER_USER_PAT, ) .await; (project, versions.into_iter().next().unwrap()) } -pub async fn add_project_beta(test_env: &TestEnvironment) -> (Project, Version) { +pub async fn add_project_beta(api: &ApiV3) -> (CommonProject, CommonVersion) { // Adds dummy data to the database with sqlx (projects, versions, threads) // Generate test project data. let jar = TestFile::DummyProjectBeta; @@ -367,13 +376,13 @@ pub async fn add_project_beta(test_env: &TestEnvironment) -> (Project, Version) .append_header(("Authorization", USER_USER_PAT)) .set_multipart(vec![json_segment.clone(), file_segment.clone()]) .to_request(); - let resp = test_env.call(req).await; + let resp = api.call(req).await; assert_eq!(resp.status(), 200); - get_project_beta(test_env).await + get_project_beta(api).await } -pub async fn add_organization_zeta(test_env: &TestEnvironment) -> Organization { +pub async fn add_organization_zeta(api: &ApiV3) -> Organization { // Add an organzation. let req = TestRequest::post() .uri("/v3/organization") @@ -383,73 +392,72 @@ pub async fn add_organization_zeta(test_env: &TestEnvironment) -> Organization { "description": "A dummy organization for testing with." })) .to_request(); - let resp = test_env.call(req).await; + let resp = api.call(req).await; assert_eq!(resp.status(), 200); - get_organization_zeta(test_env).await + get_organization_zeta(api).await } -pub async fn get_project_alpha(test_env: &TestEnvironment) -> (Project, Version) { +pub async fn get_project_alpha(api: &ApiV3) -> (CommonProject, CommonVersion) { // Get project let req = TestRequest::get() .uri("/v3/project/alpha") .append_header(("Authorization", USER_USER_PAT)) .to_request(); - let resp = test_env.call(req).await; - let project: Project = test::read_body_json(resp).await; + let resp = api.call(req).await; + let project: CommonProject = test::read_body_json(resp).await; // Get project's versions let req = TestRequest::get() .uri("/v3/project/alpha/version") .append_header(("Authorization", USER_USER_PAT)) .to_request(); - let resp = test_env.call(req).await; - let versions: Vec = test::read_body_json(resp).await; + let resp = api.call(req).await; + let versions: Vec = test::read_body_json(resp).await; let version = versions.into_iter().next().unwrap(); (project, version) } -pub async fn get_project_beta(test_env: &TestEnvironment) -> (Project, Version) { +pub async fn get_project_beta(api: &ApiV3) -> (CommonProject, CommonVersion) { // Get project let req = TestRequest::get() .uri("/v3/project/beta") .append_header(("Authorization", USER_USER_PAT)) .to_request(); - let resp = test_env.call(req).await; + let resp = api.call(req).await; assert_status(&resp, StatusCode::OK); let project: serde_json::Value = test::read_body_json(resp).await; - let project: Project = serde_json::from_value(project).unwrap(); + let project: CommonProject = serde_json::from_value(project).unwrap(); // Get project's versions let req = TestRequest::get() .uri("/v3/project/beta/version") .append_header(("Authorization", USER_USER_PAT)) .to_request(); - let resp = test_env.call(req).await; + let resp = api.call(req).await; assert_status(&resp, StatusCode::OK); - let versions: Vec = test::read_body_json(resp).await; + let versions: Vec = test::read_body_json(resp).await; let version = versions.into_iter().next().unwrap(); (project, version) } -pub async fn get_organization_zeta(test_env: &TestEnvironment) -> Organization { +pub async fn get_organization_zeta(api: &ApiV3) -> Organization { // Get organization let req = TestRequest::get() .uri("/v3/organization/zeta") .append_header(("Authorization", USER_USER_PAT)) .to_request(); - let resp = test_env.call(req).await; + let resp = api.call(req).await; let organization: Organization = test::read_body_json(resp).await; organization } -pub async fn get_oauth_client_alpha(test_env: &TestEnvironment) -> OAuthClient { - let oauth_clients = test_env - .v3 +pub async fn get_oauth_client_alpha(api: &ApiV3) -> OAuthClient { + let oauth_clients = api .get_user_oauth_clients(USER_USER_ID, USER_USER_PAT) .await; oauth_clients.into_iter().next().unwrap() diff --git a/tests/common/environment.rs b/tests/common/environment.rs index 9f91a509..13802b04 100644 --- a/tests/common/environment.rs +++ b/tests/common/environment.rs @@ -1,8 +1,7 @@ #![allow(dead_code)] -use std::{rc::Rc, sync::Arc}; - use super::{ + api_common::{generic::GenericApi, Api, ApiBuildable}, api_v2::ApiV2, api_v3::ApiV3, asserts::assert_status, @@ -11,76 +10,103 @@ use super::{ }; use crate::common::setup; use actix_http::StatusCode; -use actix_web::{dev::ServiceResponse, test, App}; +use actix_web::dev::ServiceResponse; use futures::Future; -pub async fn with_test_environment(f: impl FnOnce(TestEnvironment) -> Fut) -where +pub async fn with_test_environment( + max_connections: Option, + f: impl FnOnce(TestEnvironment) -> Fut, +) where Fut: Future, + A: ApiBuildable + 'static, { - let test_env = TestEnvironment::build(None).await; + let test_env: TestEnvironment = TestEnvironment::build(max_connections).await; let db = test_env.db.clone(); - f(test_env).await; - db.cleanup().await; } + // TODO: This needs to be slightly redesigned in order to do both V2 and v3 tests. // TODO: Most tests, since they use API functions, can be applied to both. The ones that weren't are in v2/, but // all tests that can be applied to both should use both v2 and v3 (extract api to a trait with all the API functions and call both). +pub async fn with_test_environment_all(max_connections: Option, f: F) +where + Fut: Future, + F: Fn(TestEnvironment) -> Fut, +{ + println!("Test environment: API v3"); + let test_env_api_v3 = TestEnvironment::::build(max_connections).await; + let test_env_api_v3 = TestEnvironment { + db: test_env_api_v3.db.clone(), + api: GenericApi::V3(test_env_api_v3.api), + setup_api: test_env_api_v3.setup_api, + dummy: test_env_api_v3.dummy, + }; + let db = test_env_api_v3.db.clone(); + f(test_env_api_v3).await; + db.cleanup().await; + + println!("Test environment: API v2"); + let test_env_api_v2 = TestEnvironment::::build(max_connections).await; + let test_env_api_v2 = TestEnvironment { + db: test_env_api_v2.db.clone(), + api: GenericApi::V2(test_env_api_v2.api), + setup_api: test_env_api_v2.setup_api, + dummy: test_env_api_v2.dummy, + }; + let db = test_env_api_v2.db.clone(); + f(test_env_api_v2).await; + db.cleanup().await; +} // A complete test environment, with a test actix app and a database. // Must be called in an #[actix_rt::test] context. It also simulates a // temporary sqlx db like #[sqlx::test] would. // Use .call(req) on it directly to make a test call as if test::call_service(req) were being used. #[derive(Clone)] -pub struct TestEnvironment { - test_app: Rc, // Rc as it's not Send +pub struct TestEnvironment { + // test_app: Rc, // Rc as it's not Send pub db: TemporaryDatabase, - pub v2: ApiV2, - pub v3: ApiV3, - - pub dummy: Option>, + pub api: A, + pub setup_api: ApiV3, // Used for setting up tests only (ie: in ScopesTest) + pub dummy: Option, } -impl TestEnvironment { - pub async fn build(max_connections: Option) -> Self { +impl TestEnvironment { + async fn build(max_connections: Option) -> Self { let db = TemporaryDatabase::create(max_connections).await; let mut test_env = Self::build_with_db(db).await; - let dummy = dummy_data::get_dummy_data(&test_env).await; - test_env.dummy = Some(Arc::new(dummy)); + let dummy = dummy_data::get_dummy_data(&test_env.setup_api).await; + test_env.dummy = Some(dummy); test_env } pub async fn build_with_db(db: TemporaryDatabase) -> Self { let labrinth_config = setup(&db).await; - let app = App::new().configure(|cfg| labrinth::app_config(cfg, labrinth_config.clone())); - let test_app: Rc = Rc::new(test::init_service(app).await); Self { - v2: ApiV2 { - test_app: test_app.clone(), - }, - v3: ApiV3 { - test_app: test_app.clone(), - }, - test_app, db, + api: A::build(labrinth_config.clone()).await, + setup_api: ApiV3::build(labrinth_config.clone()).await, dummy: None, + // test_app } } +} +impl TestEnvironment { pub async fn cleanup(self) { self.db.cleanup().await; } pub async fn call(&self, req: actix_http::Request) -> ServiceResponse { - self.test_app.call(req).await.unwrap() + self.api.call(req).await } + // Setup data, create a friend user notification pub async fn generate_friend_user_notification(&self) { let resp = self - .v3 + .api .add_user_to_team( &self.dummy.as_ref().unwrap().project_alpha.team_id, FRIEND_USER_ID, @@ -92,23 +118,25 @@ impl TestEnvironment { assert_status(&resp, StatusCode::NO_CONTENT); } + // Setup data, assert that a user can read notifications pub async fn assert_read_notifications_status( &self, user_id: &str, pat: &str, status_code: StatusCode, ) { - let resp = self.v3.get_user_notifications(user_id, pat).await; + let resp = self.api.get_user_notifications(user_id, pat).await; assert_status(&resp, status_code); } + // Setup data, assert that a user can read projects notifications pub async fn assert_read_user_projects_status( &self, user_id: &str, pat: &str, status_code: StatusCode, ) { - let resp = self.v3.get_user_projects(user_id, pat).await; + let resp = self.api.get_user_projects(user_id, pat).await; assert_status(&resp, status_code); } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f26a0c73..85eae894 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -2,6 +2,7 @@ use labrinth::{check_env_vars, clickhouse}; use labrinth::{file_hosting, queue, LabrinthConfig}; use std::sync::Arc; +pub mod api_common; pub mod api_v2; pub mod api_v3; pub mod asserts; diff --git a/tests/common/permissions.rs b/tests/common/permissions.rs index c960b72f..7e27def7 100644 --- a/tests/common/permissions.rs +++ b/tests/common/permissions.rs @@ -5,10 +5,14 @@ use itertools::Itertools; use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; use serde_json::json; -use crate::common::database::{generate_random_name, ADMIN_USER_PAT}; +use crate::common::{ + api_common::ApiTeams, + database::{generate_random_name, ADMIN_USER_PAT}, +}; use super::{ - api_v3::request_data, + api_common::{Api, ApiProject}, + api_v3::ApiV3, database::{ENEMY_USER_PAT, USER_USER_ID, USER_USER_PAT}, environment::TestEnvironment, }; @@ -18,8 +22,9 @@ use super::{ // - returns a 200-299 if the scope is present // - returns failure and success JSON bodies for requests that are 200 (for performing non-simple follow-up tests on) // This uses a builder format, so you can chain methods to set the parameters to non-defaults (most will probably be not need to be set). -pub struct PermissionsTest<'a> { - test_env: &'a TestEnvironment, +type JsonCheck = Box; +pub struct PermissionsTest<'a, A: Api> { + test_env: &'a TestEnvironment, // Permissions expected to fail on this test. By default, this is all permissions except the success permissions. // (To ensure we have isolated the permissions we are testing) failure_project_permissions: Option, @@ -50,12 +55,12 @@ pub struct PermissionsTest<'a> { // Closures that check the JSON body of the response for failure and success cases. // These are used to perform more complex tests than just checking the status code. // (eg: checking that the response contains the correct data) - failure_json_check: Option>, - success_json_check: Option>, + failure_json_check: Option, + success_json_check: Option, } pub struct PermissionsTestContext<'a> { - pub test_env: &'a TestEnvironment, + // pub test_env: &'a TestEnvironment, pub user_id: &'a str, pub user_pat: &'a str, pub project_id: Option<&'a str>, @@ -64,8 +69,8 @@ pub struct PermissionsTestContext<'a> { pub organization_team_id: Option<&'a str>, } -impl<'a> PermissionsTest<'a> { - pub fn new(test_env: &'a TestEnvironment) -> Self { +impl<'a, A: Api> PermissionsTest<'a, A> { + pub fn new(test_env: &'a TestEnvironment) -> Self { Self { test_env, failure_project_permissions: None, @@ -157,7 +162,6 @@ impl<'a> PermissionsTest<'a> { .failure_project_permissions .unwrap_or(ProjectPermissions::all() ^ success_permissions); let test_context = PermissionsTestContext { - test_env, user_id: self.user_id, user_pat: self.user_pat, project_id: None, @@ -172,7 +176,7 @@ impl<'a> PermissionsTest<'a> { self.project_team_id.clone().unwrap(), ) } else { - create_dummy_project(test_env).await + create_dummy_project(&test_env.setup_api).await }; add_user_to_team( @@ -181,7 +185,7 @@ impl<'a> PermissionsTest<'a> { &team_id, Some(failure_project_permissions), None, - test_env, + &test_env.setup_api, ) .await; @@ -268,7 +272,7 @@ impl<'a> PermissionsTest<'a> { &team_id, Some(success_permissions), None, - test_env, + &test_env.setup_api, ) .await; @@ -297,7 +301,7 @@ impl<'a> PermissionsTest<'a> { // If the remove_user flag is set, remove the user from the project // Relevant for existing projects/users if self.remove_user { - remove_user_from_team(self.user_id, &team_id, test_env).await; + remove_user_from_team(self.user_id, &team_id, &test_env.setup_api).await; } Ok(()) } @@ -315,7 +319,6 @@ impl<'a> PermissionsTest<'a> { .failure_organization_permissions .unwrap_or(OrganizationPermissions::all() ^ success_permissions); let test_context = PermissionsTestContext { - test_env, user_id: self.user_id, user_pat: self.user_pat, project_id: None, @@ -331,7 +334,7 @@ impl<'a> PermissionsTest<'a> { self.organization_team_id.clone().unwrap(), ) } else { - create_dummy_org(test_env).await + create_dummy_org(&test_env.setup_api).await }; add_user_to_team( @@ -340,7 +343,7 @@ impl<'a> PermissionsTest<'a> { &team_id, None, Some(failure_organization_permissions), - test_env, + &test_env.setup_api, ) .await; @@ -371,7 +374,7 @@ impl<'a> PermissionsTest<'a> { &team_id, None, Some(success_permissions), - test_env, + &test_env.setup_api, ) .await; @@ -395,7 +398,7 @@ impl<'a> PermissionsTest<'a> { // If the remove_user flag is set, remove the user from the organization // Relevant for existing projects/users if self.remove_user { - remove_user_from_team(self.user_id, &team_id, test_env).await; + remove_user_from_team(self.user_id, &team_id, &test_env.setup_api).await; } Ok(()) } @@ -413,7 +416,6 @@ impl<'a> PermissionsTest<'a> { .failure_project_permissions .unwrap_or(ProjectPermissions::all() ^ success_permissions); let test_context = PermissionsTestContext { - test_env, user_id: self.user_id, user_pat: self.user_pat, project_id: None, @@ -426,7 +428,7 @@ impl<'a> PermissionsTest<'a> { // This should always fail, regardless of permissions // (As we are testing permissions-based failures) let test_1 = async { - let (project_id, team_id) = create_dummy_project(test_env).await; + let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; let request = req_gen(&PermissionsTestContext { project_id: Some(&project_id), @@ -447,8 +449,13 @@ impl<'a> PermissionsTest<'a> { )); } - let p = - get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + let p = get_project_permissions( + self.user_id, + self.user_pat, + &project_id, + &test_env.setup_api, + ) + .await; if p != ProjectPermissions::empty() { return Err(format!( "Test 1 failed. Expected no permissions, got {:?}", @@ -462,7 +469,7 @@ impl<'a> PermissionsTest<'a> { // TEST 2: Failure // Random user, unaffiliated with the project, with no permissions let test_2 = async { - let (project_id, team_id) = create_dummy_project(test_env).await; + let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; let request = req_gen(&PermissionsTestContext { project_id: Some(&project_id), @@ -483,8 +490,13 @@ impl<'a> PermissionsTest<'a> { )); } - let p = - get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + let p = get_project_permissions( + self.user_id, + self.user_pat, + &project_id, + &test_env.setup_api, + ) + .await; if p != ProjectPermissions::empty() { return Err(format!( "Test 2 failed. Expected no permissions, got {:?}", @@ -498,14 +510,14 @@ impl<'a> PermissionsTest<'a> { // TEST 3: Failure // User affiliated with the project, with failure permissions let test_3 = async { - let (project_id, team_id) = create_dummy_project(test_env).await; + let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; add_user_to_team( self.user_id, self.user_pat, &team_id, Some(failure_project_permissions), None, - test_env, + &test_env.setup_api, ) .await; @@ -529,8 +541,13 @@ impl<'a> PermissionsTest<'a> { )); } - let p = - get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + let p = get_project_permissions( + self.user_id, + self.user_pat, + &project_id, + &test_env.setup_api, + ) + .await; if p != failure_project_permissions { return Err(format!( "Test 3 failed. Expected {:?}, got {:?}", @@ -544,14 +561,14 @@ impl<'a> PermissionsTest<'a> { // TEST 4: Success // User affiliated with the project, with the given permissions let test_4 = async { - let (project_id, team_id) = create_dummy_project(test_env).await; + let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; add_user_to_team( self.user_id, self.user_pat, &team_id, Some(success_permissions), None, - test_env, + &test_env.setup_api, ) .await; @@ -571,8 +588,13 @@ impl<'a> PermissionsTest<'a> { )); } - let p = - get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + let p = get_project_permissions( + self.user_id, + self.user_pat, + &project_id, + &test_env.setup_api, + ) + .await; if p != success_permissions { return Err(format!( "Test 4 failed. Expected {:?}, got {:?}", @@ -587,16 +609,17 @@ impl<'a> PermissionsTest<'a> { // Project has an organization // User affiliated with the project's org, with default failure permissions let test_5 = async { - let (project_id, team_id) = create_dummy_project(test_env).await; - let (organization_id, organization_team_id) = create_dummy_org(test_env).await; - add_project_to_org(test_env, &project_id, &organization_id).await; + let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; + let (organization_id, organization_team_id) = + create_dummy_org(&test_env.setup_api).await; + add_project_to_org(&test_env.setup_api, &project_id, &organization_id).await; add_user_to_team( self.user_id, self.user_pat, &organization_team_id, Some(failure_project_permissions), None, - test_env, + &test_env.setup_api, ) .await; @@ -620,8 +643,13 @@ impl<'a> PermissionsTest<'a> { )); } - let p = - get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + let p = get_project_permissions( + self.user_id, + self.user_pat, + &project_id, + &test_env.setup_api, + ) + .await; if p != failure_project_permissions { return Err(format!( "Test 5 failed. Expected {:?}, got {:?}", @@ -636,16 +664,17 @@ impl<'a> PermissionsTest<'a> { // Project has an organization // User affiliated with the project's org, with the default success let test_6 = async { - let (project_id, team_id) = create_dummy_project(test_env).await; - let (organization_id, organization_team_id) = create_dummy_org(test_env).await; - add_project_to_org(test_env, &project_id, &organization_id).await; + let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; + let (organization_id, organization_team_id) = + create_dummy_org(&test_env.setup_api).await; + add_project_to_org(&test_env.setup_api, &project_id, &organization_id).await; add_user_to_team( self.user_id, self.user_pat, &organization_team_id, Some(success_permissions), None, - test_env, + &test_env.setup_api, ) .await; @@ -665,8 +694,13 @@ impl<'a> PermissionsTest<'a> { )); } - let p = - get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + let p = get_project_permissions( + self.user_id, + self.user_pat, + &project_id, + &test_env.setup_api, + ) + .await; if p != success_permissions { return Err(format!( "Test 6 failed. Expected {:?}, got {:?}", @@ -682,16 +716,17 @@ impl<'a> PermissionsTest<'a> { // User affiliated with the project's org (even can have successful permissions!) // User overwritten on the project team with failure permissions let test_7 = async { - let (project_id, team_id) = create_dummy_project(test_env).await; - let (organization_id, organization_team_id) = create_dummy_org(test_env).await; - add_project_to_org(test_env, &project_id, &organization_id).await; + let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; + let (organization_id, organization_team_id) = + create_dummy_org(&test_env.setup_api).await; + add_project_to_org(&test_env.setup_api, &project_id, &organization_id).await; add_user_to_team( self.user_id, self.user_pat, &organization_team_id, Some(success_permissions), None, - test_env, + &test_env.setup_api, ) .await; add_user_to_team( @@ -700,7 +735,7 @@ impl<'a> PermissionsTest<'a> { &team_id, Some(failure_project_permissions), None, - test_env, + &test_env.setup_api, ) .await; @@ -724,8 +759,13 @@ impl<'a> PermissionsTest<'a> { )); } - let p = - get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + let p = get_project_permissions( + self.user_id, + self.user_pat, + &project_id, + &test_env.setup_api, + ) + .await; if p != failure_project_permissions { return Err(format!( "Test 7 failed. Expected {:?}, got {:?}", @@ -741,16 +781,17 @@ impl<'a> PermissionsTest<'a> { // User affiliated with the project's org with default failure permissions // User overwritten to the project with the success permissions let test_8 = async { - let (project_id, team_id) = create_dummy_project(test_env).await; - let (organization_id, organization_team_id) = create_dummy_org(test_env).await; - add_project_to_org(test_env, &project_id, &organization_id).await; + let (project_id, team_id) = create_dummy_project(&test_env.setup_api).await; + let (organization_id, organization_team_id) = + create_dummy_org(&test_env.setup_api).await; + add_project_to_org(&test_env.setup_api, &project_id, &organization_id).await; add_user_to_team( self.user_id, self.user_pat, &organization_team_id, Some(failure_project_permissions), None, - test_env, + &test_env.setup_api, ) .await; add_user_to_team( @@ -759,7 +800,7 @@ impl<'a> PermissionsTest<'a> { &team_id, Some(success_permissions), None, - test_env, + &test_env.setup_api, ) .await; @@ -780,8 +821,13 @@ impl<'a> PermissionsTest<'a> { )); } - let p = - get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + let p = get_project_permissions( + self.user_id, + self.user_pat, + &project_id, + &test_env.setup_api, + ) + .await; if p != success_permissions { return Err(format!( "Test 8 failed. Expected {:?}, got {:?}", @@ -811,7 +857,6 @@ impl<'a> PermissionsTest<'a> { .failure_organization_permissions .unwrap_or(OrganizationPermissions::all() ^ success_permissions); let test_context = PermissionsTestContext { - test_env, user_id: self.user_id, user_pat: self.user_pat, project_id: None, // Will be overwritten on each test @@ -823,7 +868,8 @@ impl<'a> PermissionsTest<'a> { // TEST 1: Failure // Random user, entirely unaffliaited with the organization let test_1 = async { - let (organization_id, organization_team_id) = create_dummy_org(test_env).await; + let (organization_id, organization_team_id) = + create_dummy_org(&test_env.setup_api).await; let request = req_gen(&PermissionsTestContext { organization_id: Some(&organization_id), @@ -848,7 +894,7 @@ impl<'a> PermissionsTest<'a> { self.user_id, self.user_pat, &organization_id, - test_env, + &test_env.setup_api, ) .await; if p != OrganizationPermissions::empty() { @@ -863,14 +909,15 @@ impl<'a> PermissionsTest<'a> { // TEST 2: Failure // User affiliated with the organization, with failure permissions let test_2 = async { - let (organization_id, organization_team_id) = create_dummy_org(test_env).await; + let (organization_id, organization_team_id) = + create_dummy_org(&test_env.setup_api).await; add_user_to_team( self.user_id, self.user_pat, &organization_team_id, None, Some(failure_organization_permissions), - test_env, + &test_env.setup_api, ) .await; @@ -898,7 +945,7 @@ impl<'a> PermissionsTest<'a> { self.user_id, self.user_pat, &organization_id, - test_env, + &test_env.setup_api, ) .await; if p != failure_organization_permissions { @@ -913,14 +960,15 @@ impl<'a> PermissionsTest<'a> { // TEST 3: Success // User affiliated with the organization, with the given permissions let test_3 = async { - let (organization_id, organization_team_id) = create_dummy_org(test_env).await; + let (organization_id, organization_team_id) = + create_dummy_org(&test_env.setup_api).await; add_user_to_team( self.user_id, self.user_pat, &organization_team_id, None, Some(success_permissions), - test_env, + &test_env.setup_api, ) .await; @@ -944,7 +992,7 @@ impl<'a> PermissionsTest<'a> { self.user_id, self.user_pat, &organization_id, - test_env, + &test_env.setup_api, ) .await; if p != success_permissions { @@ -962,31 +1010,29 @@ impl<'a> PermissionsTest<'a> { } } -async fn create_dummy_project(test_env: &TestEnvironment) -> (String, String) { - let api = &test_env.v3; - +async fn create_dummy_project(setup_api: &ApiV3) -> (String, String) { // Create a very simple project let slug = generate_random_name("test_project"); - let creation_data = request_data::get_public_project_creation_data(&slug, None); - let (project, _) = api.add_public_project(creation_data, ADMIN_USER_PAT).await; + let (project, _) = setup_api + .add_public_project(&slug, None, None, ADMIN_USER_PAT) + .await; let project_id = project.id.to_string(); let team_id = project.team.to_string(); (project_id, team_id) } -async fn create_dummy_org(test_env: &TestEnvironment) -> (String, String) { +async fn create_dummy_org(setup_api: &ApiV3) -> (String, String) { // Create a very simple organization let name = generate_random_name("test_org"); - let api = &test_env.v3; - let resp = api + let resp = setup_api .create_organization(&name, "Example description.", ADMIN_USER_PAT) .await; assert!(resp.status().is_success()); - let organization = api + let organization = setup_api .get_organization_deserialized(&name, ADMIN_USER_PAT) .await; let organizaion_id = organization.id.to_string(); @@ -995,9 +1041,8 @@ async fn create_dummy_org(test_env: &TestEnvironment) -> (String, String) { (organizaion_id, team_id) } -async fn add_project_to_org(test_env: &TestEnvironment, project_id: &str, organization_id: &str) { - let api = &test_env.v3; - let resp = api +async fn add_project_to_org(setup_api: &ApiV3, project_id: &str, organization_id: &str) { + let resp = setup_api .organization_add_project(organization_id, project_id, ADMIN_USER_PAT) .await; assert!(resp.status().is_success()); @@ -1009,12 +1054,10 @@ async fn add_user_to_team( team_id: &str, project_permissions: Option, organization_permissions: Option, - test_env: &TestEnvironment, + setup_api: &ApiV3, ) { - let api = &test_env.v3; - // Invite user - let resp = api + let resp = setup_api .add_user_to_team( team_id, user_id, @@ -1026,7 +1069,7 @@ async fn add_user_to_team( assert!(resp.status().is_success()); // Accept invitation - let resp = api.join_team(team_id, user_pat).await; + let resp = setup_api.join_team(team_id, user_pat).await; assert!(resp.status().is_success()); } @@ -1035,12 +1078,10 @@ async fn modify_user_team_permissions( team_id: &str, permissions: Option, organization_permissions: Option, - test_env: &TestEnvironment, + setup_api: &ApiV3, ) { - let api = &test_env.v3; - // Send invitation to user - let resp = api + let resp = setup_api .edit_team_member( team_id, user_id, @@ -1054,10 +1095,11 @@ async fn modify_user_team_permissions( assert!(resp.status().is_success()); } -async fn remove_user_from_team(user_id: &str, team_id: &str, test_env: &TestEnvironment) { +async fn remove_user_from_team(user_id: &str, team_id: &str, setup_api: &ApiV3) { // Send invitation to user - let api = &test_env.v3; - let resp = api.remove_from_team(team_id, user_id, ADMIN_USER_PAT).await; + let resp = setup_api + .remove_from_team(team_id, user_id, ADMIN_USER_PAT) + .await; assert!(resp.status().is_success()); } @@ -1065,9 +1107,9 @@ async fn get_project_permissions( user_id: &str, user_pat: &str, project_id: &str, - test_env: &TestEnvironment, + setup_api: &ApiV3, ) -> ProjectPermissions { - let resp = test_env.v3.get_project_members(project_id, user_pat).await; + let resp = setup_api.get_project_members(project_id, user_pat).await; let permissions = if resp.status().as_u16() == 200 { let value: serde_json::Value = test::read_body_json(resp).await; value @@ -1088,10 +1130,9 @@ async fn get_organization_permissions( user_id: &str, user_pat: &str, organization_id: &str, - test_env: &TestEnvironment, + setup_api: &ApiV3, ) -> OrganizationPermissions { - let api = &test_env.v3; - let resp = api + let resp = setup_api .get_organization_members(organization_id, user_pat) .await; let permissions = if resp.status().as_u16() == 200 { diff --git a/tests/common/scopes.rs b/tests/common/scopes.rs index f58c85e8..c43ee98e 100644 --- a/tests/common/scopes.rs +++ b/tests/common/scopes.rs @@ -2,15 +2,18 @@ use actix_web::test::{self, TestRequest}; use labrinth::models::pats::Scopes; -use super::{database::USER_USER_ID_PARSED, environment::TestEnvironment, pats::create_test_pat}; +use super::{ + api_common::Api, database::USER_USER_ID_PARSED, environment::TestEnvironment, + pats::create_test_pat, +}; // A reusable test type that works for any scope test testing an endpoint that: // - returns a known 'expected_failure_code' if the scope is not present (defaults to 401) // - returns a 200-299 if the scope is present // - returns failure and success JSON bodies for requests that are 200 (for performing non-simple follow-up tests on) // This uses a builder format, so you can chain methods to set the parameters to non-defaults (most will probably be not need to be set). -pub struct ScopeTest<'a> { - test_env: &'a TestEnvironment, +pub struct ScopeTest<'a, A> { + test_env: &'a TestEnvironment, // Scopes expected to fail on this test. By default, this is all scopes except the success scopes. // (To ensure we have isolated the scope we are testing) failure_scopes: Option, @@ -20,8 +23,8 @@ pub struct ScopeTest<'a> { expected_failure_code: u16, } -impl<'a> ScopeTest<'a> { - pub fn new(test_env: &'a TestEnvironment) -> Self { +impl<'a, A: Api> ScopeTest<'a, A> { + pub fn new(test_env: &'a TestEnvironment) -> Self { Self { test_env, failure_scopes: None, diff --git a/tests/games.rs b/tests/games.rs index 14ddb378..75c5671a 100644 --- a/tests/games.rs +++ b/tests/games.rs @@ -1,23 +1,26 @@ // TODO: fold this into loader_fields.rs or tags.rs of other v3 testing PR -use crate::common::environment::TestEnvironment; +use common::{ + api_v3::ApiV3, + environment::{with_test_environment, TestEnvironment}, +}; mod common; #[actix_rt::test] async fn get_games() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = test_env.api; - let games = api.get_games_deserialized().await; + let games = api.get_games_deserialized().await; - // There should be 2 games in the dummy data - assert_eq!(games.len(), 2); - assert_eq!(games[0].name, "minecraft-java"); - assert_eq!(games[1].name, "minecraft-bedrock"); + // There should be 2 games in the dummy data + assert_eq!(games.len(), 2); + assert_eq!(games[0].name, "minecraft-java"); + assert_eq!(games[1].name, "minecraft-bedrock"); - assert_eq!(games[0].slug, "minecraft-java"); - assert_eq!(games[1].slug, "minecraft-bedrock"); - - test_env.cleanup().await; + assert_eq!(games[0].slug, "minecraft-java"); + assert_eq!(games[1].slug, "minecraft-bedrock"); + }) + .await; } diff --git a/tests/loader_fields.rs b/tests/loader_fields.rs index b7f65e34..14bb3812 100644 --- a/tests/loader_fields.rs +++ b/tests/loader_fields.rs @@ -1,9 +1,10 @@ use std::collections::HashSet; -use common::environment::TestEnvironment; +use common::api_v3::ApiV3; +use common::environment::{with_test_environment, TestEnvironment}; use serde_json::json; -use crate::common::api_v3::request_data::get_public_version_creation_data; +use crate::common::api_common::ApiVersion; use crate::common::database::*; use crate::common::dummy_data::TestFile; @@ -13,155 +14,156 @@ mod common; #[actix_rt::test] async fn creating_loader_fields() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; - let alpha_project_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_alpha - .project_id - .clone(); - let alpha_project_id = serde_json::from_str(&format!("\"{}\"", alpha_project_id)).unwrap(); - let alpha_version_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_alpha - .version_id - .clone(); + let alpha_project_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .project_id + .clone(); + let alpha_project_id = serde_json::from_str(&format!("\"{}\"", alpha_project_id)).unwrap(); + let alpha_version_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .version_id + .clone(); - // ALL THE FOLLOWING FOR CREATE AND PATCH - // Cannot create a version with an extra argument that cannot be tied to a loader field ("invalid loader field") - // TODO: - Create project - // - Create version - let version_data = get_public_version_creation_data( - alpha_project_id, - "1.0.0", - TestFile::build_random_jar(), - Some(|j: &mut serde_json::Value| { - j["invalid"] = json!("invalid"); - }), - ); - let resp = api.add_public_version(version_data, USER_USER_PAT).await; - assert_eq!(resp.status(), 400); - // - Patch - let resp = api - .edit_version( - alpha_version_id, - json!({ - "invalid": "invalid" - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 400); - - // Cannot create a version with a loader field that isnt used by the loader - // TODO: - Create project - // - Create version - let version_data = get_public_version_creation_data( - alpha_project_id, - "1.0.0", - TestFile::build_random_jar(), - Some(|j: &mut serde_json::Value| { - // This is only for mrpacks, not mods/jars - j["mrpack_loaders"] = json!(["fabric"]); - }), - ); - let resp = api.add_public_version(version_data, USER_USER_PAT).await; - assert_eq!(resp.status(), 400); - // - Patch - let resp = api - .edit_version( - alpha_version_id, - json!({ - "mrpack_loaders": ["fabric"] - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 400); + // ALL THE FOLLOWING FOR CREATE AND PATCH + // Cannot create a version with an extra argument that cannot be tied to a loader field ("invalid loader field") + // TODO: - Create project + // - Create version + let resp = api + .add_public_version( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + None, + Some( + serde_json::from_value(json!([{ + "op": "add", + "path": "/invalid", + "value": "invalid" + }])) + .unwrap(), + ), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + // - Patch + let resp = api + .edit_version( + alpha_version_id, + json!({ + "invalid": "invalid" + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); - // Cannot create a version without an applicable loader field that is not optional - // TODO: - Create project - // - Create version - let version_data = get_public_version_creation_data( - alpha_project_id, - "1.0.0", - TestFile::build_random_jar(), - Some(|j: &mut serde_json::Value| { - let j = j.as_object_mut().unwrap(); - j.remove("client_side"); - }), - ); - let resp = api.add_public_version(version_data, USER_USER_PAT).await; - assert_eq!(resp.status(), 400); + // Cannot create a version with a loader field that isnt used by the loader + // TODO: - Create project + // - Create version + let resp = api + .add_public_version( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + None, + Some( + serde_json::from_value(json!([{ + "op": "add", + "path": "/mrpack_loaders", + "value": ["fabric"] + }])) + .unwrap(), + ), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + // - Patch + let resp = api + .edit_version( + alpha_version_id, + json!({ + "mrpack_loaders": ["fabric"] + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); - // Cannot create a version without a loader field array that has a minimum of 1 - // TODO: - Create project - // - Create version - let version_data = get_public_version_creation_data( - alpha_project_id, - "1.0.0", - TestFile::build_random_jar(), - Some(|j: &mut serde_json::Value| { - let j = j.as_object_mut().unwrap(); - j.remove("game_versions"); - }), - ); - let resp = api.add_public_version(version_data, USER_USER_PAT).await; - assert_eq!(resp.status(), 400); + // Cannot create a version without an applicable loader field that is not optional + // TODO: - Create project + // - Create version + let resp = api + .add_public_version( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + None, + Some( + serde_json::from_value(json!([{ + "op": "remove", + "path": "/client_side" + }])) + .unwrap(), + ), + USER_USER_PAT, + ) + .await; - // TODO: Create a test for too many elements in the array when we have a LF that has a max (past max) - // Cannot create a version with a loader field array that has fewer than the minimum elements - // TODO: - Create project - // - Create version - let version_data = get_public_version_creation_data( - alpha_project_id, - "1.0.0", - TestFile::build_random_jar(), - Some(|j: &mut serde_json::Value| { - let j: &mut serde_json::Map = j.as_object_mut().unwrap(); - j["game_versions"] = json!([]); - }), - ); - let resp: actix_web::dev::ServiceResponse = - api.add_public_version(version_data, USER_USER_PAT).await; - assert_eq!(resp.status(), 400); + assert_eq!(resp.status(), 400); - // - Patch - let resp = api - .edit_version( - alpha_version_id, - json!({ - "game_versions": [] - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 400); + // Cannot create a version without a loader field array that has a minimum of 1 + // TODO: - Create project + // - Create version + let resp = api + .add_public_version( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + None, + Some( + serde_json::from_value(json!([{ + "op": "remove", + "path": "/game_versions" + }])) + .unwrap(), + ), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); - // Cannot create an invalid data type for the loader field type (including bad variant for the type) - for bad_type_game_versions in [ - json!(1), - json!([1]), - json!("1.20.1"), - json!(["client_side"]), - ] { + // TODO: Create a test for too many elements in the array when we have a LF that has a max (past max) + // Cannot create a version with a loader field array that has fewer than the minimum elements // TODO: - Create project // - Create version - let version_data = get_public_version_creation_data( - alpha_project_id, - "1.0.0", - TestFile::build_random_jar(), - Some(|j: &mut serde_json::Value| { - let j: &mut serde_json::Map = j.as_object_mut().unwrap(); - j["game_versions"] = bad_type_game_versions.clone(); - }), - ); - let resp = api.add_public_version(version_data, USER_USER_PAT).await; + let resp: actix_web::dev::ServiceResponse = api + .add_public_version( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + None, + Some( + serde_json::from_value(json!([{ + "op": "add", + "path": "/game_versions", + "value": [] + }])) + .unwrap(), + ), + USER_USER_PAT, + ) + .await; assert_eq!(resp.status(), 400); // - Patch @@ -169,138 +171,195 @@ async fn creating_loader_fields() { .edit_version( alpha_version_id, json!({ - "game_versions": bad_type_game_versions + "game_versions": [] }), USER_USER_PAT, ) .await; assert_eq!(resp.status(), 400); - } - // Can create with optional loader fields (other tests have checked if we can create without them) - // TODO: - Create project - // - Create version - let version_data = get_public_version_creation_data( - alpha_project_id, - "1.0.0", - TestFile::build_random_jar(), - Some(|j: &mut serde_json::Value| { - j["test_fabric_optional"] = json!(555); - }), - ); - let v = api - .add_public_version_deserialized(version_data, USER_USER_PAT) - .await; - assert_eq!(v.fields.get("test_fabric_optional").unwrap(), &json!(555)); - // - Patch - let resp = api - .edit_version( - alpha_version_id, - json!({ - "test_fabric_optional": 555 - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - let v = api - .get_version_deserialized(alpha_version_id, USER_USER_PAT) - .await; - assert_eq!(v.fields.get("test_fabric_optional").unwrap(), &json!(555)); + // Cannot create an invalid data type for the loader field type (including bad variant for the type) + for bad_type_game_versions in [ + json!(1), + json!([1]), + json!("1.20.1"), + json!(["client_side"]), + ] { + // TODO: - Create project + // - Create version + let resp = api + .add_public_version( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + None, + Some( + serde_json::from_value(json!([{ + "op": "add", + "path": "/game_versions", + "value": bad_type_game_versions + }])) + .unwrap(), + ), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + + // - Patch + let resp = api + .edit_version( + alpha_version_id, + json!({ + "game_versions": bad_type_game_versions + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } - // Simply setting them as expected works - // - Create - let version_data = get_public_version_creation_data( - alpha_project_id, - "1.0.0", - TestFile::build_random_jar(), - Some(|j: &mut serde_json::Value| { - let j: &mut serde_json::Map = j.as_object_mut().unwrap(); - j["game_versions"] = json!(["1.20.1", "1.20.2"]); - j["client_side"] = json!("optional"); - j["server_side"] = json!("required"); - }), - ); - let v = api - .add_public_version_deserialized(version_data, USER_USER_PAT) - .await; - assert_eq!( - v.fields.get("game_versions").unwrap(), - &json!(["1.20.1", "1.20.2"]) - ); - assert_eq!(v.fields.get("client_side").unwrap(), &json!("optional")); - assert_eq!(v.fields.get("server_side").unwrap(), &json!("required")); - // - Patch - let resp = api - .edit_version( - alpha_version_id, - json!({ - "game_versions": ["1.20.1", "1.20.2"], - "client_side": "optional", - "server_side": "required" - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - let v = api - .get_version_deserialized(alpha_version_id, USER_USER_PAT) - .await; - assert_eq!( - v.fields.get("game_versions").unwrap(), - &json!(["1.20.1", "1.20.2"]) - ); + // Can create with optional loader fields (other tests have checked if we can create without them) + // TODO: - Create project + // - Create version + let v = api + .add_public_version_deserialized( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + None, + Some( + serde_json::from_value(json!([{ + "op": "add", + "path": "/test_fabric_optional", + "value": 555 + }])) + .unwrap(), + ), + USER_USER_PAT, + ) + .await; + assert_eq!(v.fields.get("test_fabric_optional").unwrap(), &json!(555)); + // - Patch + let resp = api + .edit_version( + alpha_version_id, + json!({ + "test_fabric_optional": 555 + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + let v = api + .get_version_deserialized(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!(v.fields.get("test_fabric_optional").unwrap(), &json!(555)); - test_env.cleanup().await; + // Simply setting them as expected works + // - Create + let v = api + .add_public_version_deserialized( + alpha_project_id, + "1.0.0", + TestFile::build_random_jar(), + None, + Some( + serde_json::from_value(json!([{ + "op": "add", + "path": "/game_versions", + "value": ["1.20.1", "1.20.2"] + }, { + "op": "add", + "path": "/client_side", + "value": "optional" + }, { + "op": "add", + "path": "/server_side", + "value": "required" + }])) + .unwrap(), + ), + USER_USER_PAT, + ) + .await; + assert_eq!( + v.fields.get("game_versions").unwrap(), + &json!(["1.20.1", "1.20.2"]) + ); + assert_eq!(v.fields.get("client_side").unwrap(), &json!("optional")); + assert_eq!(v.fields.get("server_side").unwrap(), &json!("required")); + // - Patch + let resp = api + .edit_version( + alpha_version_id, + json!({ + "game_versions": ["1.20.1", "1.20.2"], + "client_side": "optional", + "server_side": "required" + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + let v = api + .get_version_deserialized(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!( + v.fields.get("game_versions").unwrap(), + &json!(["1.20.1", "1.20.2"]) + ); + }) + .await } #[actix_rt::test] async fn get_loader_fields() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; - let game_versions = api - .get_loader_field_variants_deserialized("game_versions") - .await; - let side_types = api - .get_loader_field_variants_deserialized("client_side") - .await; - - // These tests match dummy data and will need to be updated if the dummy data changes - // Versions should be ordered by: - // - ordering - // - ordering ties settled by date added to database - // - We also expect presentation of NEWEST to OLDEST - // - All null orderings are treated as older than any non-null ordering - // (for this test, the 1.20.1, etc, versions are all null ordering) - let game_version_versions = game_versions - .into_iter() - .map(|x| x.value) - .collect::>(); - assert_eq!( - game_version_versions, - [ - "Ordering_Negative1", - "Ordering_Positive100", - "1.20.5", - "1.20.4", - "1.20.3", - "1.20.2", - "1.20.1" - ] - ); + let game_versions = api + .get_loader_field_variants_deserialized("game_versions") + .await; + let side_types = api + .get_loader_field_variants_deserialized("client_side") + .await; - let side_type_names = side_types - .into_iter() - .map(|x| x.value) - .collect::>(); - assert_eq!( - side_type_names, - ["unknown", "required", "optional", "unsupported"] - .iter() - .map(|s| s.to_string()) - .collect() - ); + // These tests match dummy data and will need to be updated if the dummy data changes + // Versions should be ordered by: + // - ordering + // - ordering ties settled by date added to database + // - We also expect presentation of NEWEST to OLDEST + // - All null orderings are treated as older than any non-null ordering + // (for this test, the 1.20.1, etc, versions are all null ordering) + let game_version_versions = game_versions + .into_iter() + .map(|x| x.value) + .collect::>(); + assert_eq!( + game_version_versions, + [ + "Ordering_Negative1", + "Ordering_Positive100", + "1.20.5", + "1.20.4", + "1.20.3", + "1.20.2", + "1.20.1" + ] + ); - test_env.cleanup().await; + let side_type_names = side_types + .into_iter() + .map(|x| x.value) + .collect::>(); + assert_eq!( + side_type_names, + ["unknown", "required", "optional", "unsupported"] + .iter() + .map(|s| s.to_string()) + .collect() + ); + }) + .await } diff --git a/tests/notifications.rs b/tests/notifications.rs index 52a71755..0d246338 100644 --- a/tests/notifications.rs +++ b/tests/notifications.rs @@ -1,13 +1,15 @@ use common::{ database::{FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_PAT}, - environment::with_test_environment, + environment::with_test_environment_all, }; +use crate::common::api_common::ApiTeams; + mod common; #[actix_rt::test] pub async fn get_user_notifications_after_team_invitation_returns_notification() { - with_test_environment(|test_env| async move { + with_test_environment_all(None, |test_env| async move { let alpha_team_id = test_env .dummy .as_ref() @@ -15,15 +17,15 @@ pub async fn get_user_notifications_after_team_invitation_returns_notification() .project_alpha .team_id .clone(); - let api = test_env.v3; - api.get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) + let api = test_env.api; + api.get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) .await; api.add_user_to_team(&alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) .await; let notifications = api - .get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) .await; assert_eq!(1, notifications.len()); }) @@ -32,11 +34,11 @@ pub async fn get_user_notifications_after_team_invitation_returns_notification() #[actix_rt::test] pub async fn get_user_notifications_after_reading_indicates_notification_read() { - with_test_environment(|test_env| async move { + with_test_environment_all(None, |test_env| async move { test_env.generate_friend_user_notification().await; - let api = test_env.v3; + let api = test_env.api; let notifications = api - .get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) .await; assert_eq!(1, notifications.len()); let notification_id = notifications[0].id.to_string(); @@ -45,7 +47,7 @@ pub async fn get_user_notifications_after_reading_indicates_notification_read() .await; let notifications = api - .get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) .await; assert_eq!(1, notifications.len()); assert!(notifications[0].read); @@ -55,11 +57,11 @@ pub async fn get_user_notifications_after_reading_indicates_notification_read() #[actix_rt::test] pub async fn get_user_notifications_after_deleting_does_not_show_notification() { - with_test_environment(|test_env| async move { + with_test_environment_all(None, |test_env| async move { test_env.generate_friend_user_notification().await; - let api = test_env.v3; + let api = test_env.api; let notifications = api - .get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) .await; assert_eq!(1, notifications.len()); let notification_id = notifications[0].id.to_string(); @@ -68,7 +70,7 @@ pub async fn get_user_notifications_after_deleting_does_not_show_notification() .await; let notifications = api - .get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) .await; assert_eq!(0, notifications.len()); }) diff --git a/tests/oauth.rs b/tests/oauth.rs index 76e7ad85..da6b29a8 100644 --- a/tests/oauth.rs +++ b/tests/oauth.rs @@ -2,12 +2,15 @@ use actix_http::StatusCode; use actix_web::test; use common::{ api_v3::oauth::get_redirect_location_query_params, - api_v3::oauth::{get_auth_code_from_redirect_params, get_authorize_accept_flow_id}, + api_v3::{ + oauth::{get_auth_code_from_redirect_params, get_authorize_accept_flow_id}, + ApiV3, + }, asserts::{assert_any_status_except, assert_status}, database::FRIEND_USER_ID, database::{FRIEND_USER_PAT, USER_USER_ID, USER_USER_PAT}, dummy_data::DummyOAuthClientAlpha, - environment::with_test_environment, + environment::{with_test_environment, TestEnvironment}, }; use labrinth::auth::oauth::TokenResponse; use reqwest::header::{CACHE_CONTROL, PRAGMA}; @@ -16,7 +19,7 @@ mod common; #[actix_rt::test] async fn oauth_flow_happy_path() { - with_test_environment(|env| async move { + with_test_environment(None, |env: TestEnvironment| async move { let DummyOAuthClientAlpha { valid_redirect_uri: base_redirect_uri, client_id, @@ -27,7 +30,7 @@ async fn oauth_flow_happy_path() { let redirect_uri = format!("{}?foo=bar", base_redirect_uri); let original_state = "1234"; let resp = env - .v3 + .api .oauth_authorize( &client_id, Some("USER_READ NOTIFICATION_READ"), @@ -40,7 +43,7 @@ async fn oauth_flow_happy_path() { let flow_id = get_authorize_accept_flow_id(resp).await; // Accept the authorization request - let resp = env.v3.oauth_accept(&flow_id, FRIEND_USER_PAT).await; + let resp = env.api.oauth_accept(&flow_id, FRIEND_USER_PAT).await; assert_status(&resp, StatusCode::OK); let query = get_redirect_location_query_params(&resp); @@ -52,7 +55,7 @@ async fn oauth_flow_happy_path() { // Get the token let resp = env - .v3 + .api .oauth_token( auth_code.to_string(), Some(redirect_uri.clone()), @@ -78,11 +81,11 @@ async fn oauth_flow_happy_path() { #[actix_rt::test] async fn oauth_authorize_for_already_authorized_scopes_returns_auth_code() { - with_test_environment(|env| async { + with_test_environment(None, |env: TestEnvironment| async move { let DummyOAuthClientAlpha { client_id, .. } = env.dummy.unwrap().oauth_client_alpha.clone(); let resp = env - .v3 + .api .oauth_authorize( &client_id, Some("USER_READ NOTIFICATION_READ"), @@ -92,10 +95,10 @@ async fn oauth_authorize_for_already_authorized_scopes_returns_auth_code() { ) .await; let flow_id = get_authorize_accept_flow_id(resp).await; - env.v3.oauth_accept(&flow_id, USER_USER_PAT).await; + env.api.oauth_accept(&flow_id, USER_USER_PAT).await; let resp = env - .v3 + .api .oauth_authorize( &client_id, Some("USER_READ"), @@ -111,7 +114,7 @@ async fn oauth_authorize_for_already_authorized_scopes_returns_auth_code() { #[actix_rt::test] async fn get_oauth_token_with_already_used_auth_code_fails() { - with_test_environment(|env| async { + with_test_environment(None, |env: TestEnvironment| async move { let DummyOAuthClientAlpha { client_id, client_secret, @@ -119,22 +122,22 @@ async fn get_oauth_token_with_already_used_auth_code_fails() { } = env.dummy.unwrap().oauth_client_alpha.clone(); let resp = env - .v3 + .api .oauth_authorize(&client_id, None, None, None, USER_USER_PAT) .await; let flow_id = get_authorize_accept_flow_id(resp).await; - let resp = env.v3.oauth_accept(&flow_id, USER_USER_PAT).await; + let resp = env.api.oauth_accept(&flow_id, USER_USER_PAT).await; let auth_code = get_auth_code_from_redirect_params(&resp).await; let resp = env - .v3 + .api .oauth_token(auth_code.clone(), None, client_id.clone(), &client_secret) .await; assert_status(&resp, StatusCode::OK); let resp = env - .v3 + .api .oauth_token(auth_code, None, client_id, &client_secret) .await; assert_status(&resp, StatusCode::BAD_REQUEST); @@ -144,7 +147,7 @@ async fn get_oauth_token_with_already_used_auth_code_fails() { #[actix_rt::test] async fn authorize_with_broader_scopes_can_complete_flow() { - with_test_environment(|env| async move { + with_test_environment(None, |env: TestEnvironment| async move { let DummyOAuthClientAlpha { client_id, client_secret, @@ -152,7 +155,7 @@ async fn authorize_with_broader_scopes_can_complete_flow() { } = env.dummy.as_ref().unwrap().oauth_client_alpha.clone(); let first_access_token = env - .v3 + .api .complete_full_authorize_flow( &client_id, &client_secret, @@ -163,7 +166,7 @@ async fn authorize_with_broader_scopes_can_complete_flow() { ) .await; let second_access_token = env - .v3 + .api .complete_full_authorize_flow( &client_id, &client_secret, @@ -193,17 +196,17 @@ async fn authorize_with_broader_scopes_can_complete_flow() { #[actix_rt::test] async fn oauth_authorize_with_broader_scopes_requires_user_accept() { - with_test_environment(|env| async { + with_test_environment(None, |env: TestEnvironment| async move { let client_id = env.dummy.unwrap().oauth_client_alpha.client_id.clone(); let resp = env - .v3 + .api .oauth_authorize(&client_id, Some("USER_READ"), None, None, USER_USER_PAT) .await; let flow_id = get_authorize_accept_flow_id(resp).await; - env.v3.oauth_accept(&flow_id, USER_USER_PAT).await; + env.api.oauth_accept(&flow_id, USER_USER_PAT).await; let resp = env - .v3 + .api .oauth_authorize( &client_id, Some("USER_READ NOTIFICATION_READ"), @@ -221,18 +224,18 @@ async fn oauth_authorize_with_broader_scopes_requires_user_accept() { #[actix_rt::test] async fn reject_authorize_ends_authorize_flow() { - with_test_environment(|env| async move { + with_test_environment(None, |env: TestEnvironment| async move { let client_id = env.dummy.unwrap().oauth_client_alpha.client_id.clone(); let resp = env - .v3 + .api .oauth_authorize(&client_id, None, None, None, USER_USER_PAT) .await; let flow_id = get_authorize_accept_flow_id(resp).await; - let resp = env.v3.oauth_reject(&flow_id, USER_USER_PAT).await; + let resp = env.api.oauth_reject(&flow_id, USER_USER_PAT).await; assert_status(&resp, StatusCode::OK); - let resp = env.v3.oauth_accept(&flow_id, USER_USER_PAT).await; + let resp = env.api.oauth_accept(&flow_id, USER_USER_PAT).await; assert_any_status_except(&resp, StatusCode::OK); }) .await; @@ -240,17 +243,17 @@ async fn reject_authorize_ends_authorize_flow() { #[actix_rt::test] async fn accept_authorize_after_already_accepting_fails() { - with_test_environment(|env| async move { + with_test_environment(None, |env: TestEnvironment| async move { let client_id = env.dummy.unwrap().oauth_client_alpha.client_id.clone(); let resp = env - .v3 + .api .oauth_authorize(&client_id, None, None, None, USER_USER_PAT) .await; let flow_id = get_authorize_accept_flow_id(resp).await; - let resp = env.v3.oauth_accept(&flow_id, USER_USER_PAT).await; + let resp = env.api.oauth_accept(&flow_id, USER_USER_PAT).await; assert_status(&resp, StatusCode::OK); - let resp = env.v3.oauth_accept(&flow_id, USER_USER_PAT).await; + let resp = env.api.oauth_accept(&flow_id, USER_USER_PAT).await; assert_status(&resp, StatusCode::BAD_REQUEST); }) .await; @@ -258,14 +261,14 @@ async fn accept_authorize_after_already_accepting_fails() { #[actix_rt::test] async fn revoke_authorization_after_issuing_token_revokes_token() { - with_test_environment(|env| async move { + with_test_environment(None, |env: TestEnvironment| async move { let DummyOAuthClientAlpha { client_id, client_secret, .. } = env.dummy.as_ref().unwrap().oauth_client_alpha.clone(); let access_token = env - .v3 + .api .complete_full_authorize_flow( &client_id, &client_secret, @@ -279,7 +282,7 @@ async fn revoke_authorization_after_issuing_token_revokes_token() { .await; let resp = env - .v3 + .api .revoke_oauth_authorization(&client_id, USER_USER_PAT) .await; assert_status(&resp, StatusCode::OK); diff --git a/tests/oauth_clients.rs b/tests/oauth_clients.rs index d4eef9f1..6f42c433 100644 --- a/tests/oauth_clients.rs +++ b/tests/oauth_clients.rs @@ -1,9 +1,10 @@ use actix_http::StatusCode; use actix_web::test; use common::{ + api_v3::ApiV3, database::{FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_ID, USER_USER_PAT}, dummy_data::DummyOAuthClientAlpha, - environment::with_test_environment, + environment::{with_test_environment, TestEnvironment}, get_json_val_str, }; use labrinth::{ @@ -20,14 +21,14 @@ mod common; #[actix_rt::test] async fn can_create_edit_get_oauth_client() { - with_test_environment(|env| async move { + with_test_environment(None, |env: TestEnvironment| async move { let client_name = "test_client".to_string(); let redirect_uris = vec![ "https://modrinth.com".to_string(), "https://modrinth.com/a".to_string(), ]; let resp = env - .v3 + .api .add_oauth_client( client_name.clone(), Scopes::all() - Scopes::restricted(), @@ -51,13 +52,13 @@ async fn can_create_edit_get_oauth_client() { redirect_uris: Some(edited_redirect_uris.clone()), }; let resp = env - .v3 + .api .edit_oauth_client(&client_id, edit, FRIEND_USER_PAT) .await; assert_status(&resp, StatusCode::OK); let clients = env - .v3 + .api .get_user_oauth_clients(FRIEND_USER_ID, FRIEND_USER_PAT) .await; assert_eq!(1, clients.len()); @@ -72,9 +73,9 @@ async fn can_create_edit_get_oauth_client() { #[actix_rt::test] async fn create_oauth_client_with_restricted_scopes_fails() { - with_test_environment(|env| async move { + with_test_environment(None, |env: TestEnvironment| async move { let resp = env - .v3 + .api .add_oauth_client( "test_client".to_string(), Scopes::restricted(), @@ -90,12 +91,12 @@ async fn create_oauth_client_with_restricted_scopes_fails() { #[actix_rt::test] async fn get_oauth_client_for_client_creator_succeeds() { - with_test_environment(|env| async move { + with_test_environment(None, |env: TestEnvironment| async move { let DummyOAuthClientAlpha { client_id, .. } = env.dummy.as_ref().unwrap().oauth_client_alpha.clone(); let resp = env - .v3 + .api .get_oauth_client(client_id.clone(), USER_USER_PAT) .await; @@ -108,12 +109,12 @@ async fn get_oauth_client_for_client_creator_succeeds() { #[actix_rt::test] async fn get_oauth_client_for_unrelated_user_fails() { - with_test_environment(|env| async move { + with_test_environment(None, |env: TestEnvironment| async move { let DummyOAuthClientAlpha { client_id, .. } = env.dummy.as_ref().unwrap().oauth_client_alpha.clone(); let resp = env - .v3 + .api .get_oauth_client(client_id.clone(), FRIEND_USER_PAT) .await; @@ -124,13 +125,13 @@ async fn get_oauth_client_for_unrelated_user_fails() { #[actix_rt::test] async fn can_delete_oauth_client() { - with_test_environment(|env| async move { + with_test_environment(None, |env: TestEnvironment| async move { let client_id = env.dummy.unwrap().oauth_client_alpha.client_id.clone(); - let resp = env.v3.delete_oauth_client(&client_id, USER_USER_PAT).await; + let resp = env.api.delete_oauth_client(&client_id, USER_USER_PAT).await; assert_status(&resp, StatusCode::NO_CONTENT); let clients = env - .v3 + .api .get_user_oauth_clients(USER_USER_ID, USER_USER_PAT) .await; assert_eq!(0, clients.len()); @@ -140,14 +141,14 @@ async fn can_delete_oauth_client() { #[actix_rt::test] async fn delete_oauth_client_after_issuing_access_tokens_revokes_tokens() { - with_test_environment(|env| async move { + with_test_environment(None, |env: TestEnvironment| async move { let DummyOAuthClientAlpha { client_id, client_secret, .. } = env.dummy.as_ref().unwrap().oauth_client_alpha.clone(); let access_token = env - .v3 + .api .complete_full_authorize_flow( &client_id, &client_secret, @@ -158,7 +159,7 @@ async fn delete_oauth_client_after_issuing_access_tokens_revokes_tokens() { ) .await; - env.v3.delete_oauth_client(&client_id, USER_USER_PAT).await; + env.api.delete_oauth_client(&client_id, USER_USER_PAT).await; env.assert_read_notifications_status(USER_USER_ID, &access_token, StatusCode::UNAUTHORIZED) .await; @@ -168,13 +169,13 @@ async fn delete_oauth_client_after_issuing_access_tokens_revokes_tokens() { #[actix_rt::test] async fn can_list_user_oauth_authorizations() { - with_test_environment(|env| async move { + with_test_environment(None, |env: TestEnvironment| async move { let DummyOAuthClientAlpha { client_id, client_secret, .. } = env.dummy.as_ref().unwrap().oauth_client_alpha.clone(); - env.v3 + env.api .complete_full_authorize_flow( &client_id, &client_secret, @@ -185,7 +186,7 @@ async fn can_list_user_oauth_authorizations() { ) .await; - let authorizations = env.v3.get_user_oauth_authorizations(USER_USER_PAT).await; + let authorizations = env.api.get_user_oauth_authorizations(USER_USER_PAT).await; assert_eq!(1, authorizations.len()); assert_eq!(USER_USER_ID_PARSED, authorizations[0].user_id.0 as i64); }) diff --git a/tests/organizations.rs b/tests/organizations.rs index 66ef7def..e12c1ea4 100644 --- a/tests/organizations.rs +++ b/tests/organizations.rs @@ -1,13 +1,15 @@ use crate::common::{ + api_common::ApiTeams, api_v3::request_data::get_icon_data, database::{generate_random_name, ADMIN_USER_PAT, MOD_USER_ID, MOD_USER_PAT, USER_USER_ID}, dummy_data::DummyImage, - environment::TestEnvironment, }; use actix_web::test; use bytes::Bytes; use common::{ + api_v3::ApiV3, database::{FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_PAT}, + environment::{with_test_environment, with_test_environment_all, TestEnvironment}, permissions::{PermissionsTest, PermissionsTestContext}, }; use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; @@ -17,633 +19,633 @@ mod common; #[actix_rt::test] async fn create_organization() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; - let zeta_organization_slug = &test_env - .dummy - .as_ref() - .unwrap() - .organization_zeta - .organization_id; - - // Failed creations title: - // - slug collision with zeta - // - too short slug - // - too long slug - // - not url safe slug - for title in [ - zeta_organization_slug, - "a", - &"a".repeat(100), - "not url safe%&^!#$##!@#$%^&*()", - ] { + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + let zeta_organization_slug = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + + // Failed creations title: + // - slug collision with zeta + // - too short slug + // - too long slug + // - not url safe slug + for title in [ + zeta_organization_slug, + "a", + &"a".repeat(100), + "not url safe%&^!#$##!@#$%^&*()", + ] { + let resp = api + .create_organization(title, "theta_description", USER_USER_PAT) + .await; + assert_eq!(resp.status(), 400); + } + + // Failed creations description: + // - too short slug + // - too long slug + for description in ["a", &"a".repeat(300)] { + let resp = api + .create_organization("theta", description, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 400); + } + + // Create 'theta' organization let resp = api - .create_organization(title, "theta_description", USER_USER_PAT) + .create_organization("theta", "not url safe%&^!#$##!@#$%^&", USER_USER_PAT) .await; - assert_eq!(resp.status(), 400); - } + assert_eq!(resp.status(), 200); - // Failed creations description: - // - too short slug - // - too long slug - for description in ["a", &"a".repeat(300)] { - let resp = api - .create_organization("theta", description, USER_USER_PAT) + // Get organization using slug + let theta = api + .get_organization_deserialized("theta", USER_USER_PAT) + .await; + assert_eq!(theta.title, "theta"); + assert_eq!(theta.description, "not url safe%&^!#$##!@#$%^&"); + assert_eq!(resp.status(), 200); + + // Get created team + let members = api + .get_organization_members_deserialized_common("theta", USER_USER_PAT) .await; - assert_eq!(resp.status(), 400); - } - - // Create 'theta' organization - let resp = api - .create_organization("theta", "not url safe%&^!#$##!@#$%^&", USER_USER_PAT) - .await; - assert_eq!(resp.status(), 200); - - // Get organization using slug - let theta = api - .get_organization_deserialized("theta", USER_USER_PAT) - .await; - assert_eq!(theta.title, "theta"); - assert_eq!(theta.description, "not url safe%&^!#$##!@#$%^&"); - assert_eq!(resp.status(), 200); - - // Get created team - let members = api - .get_organization_members_deserialized("theta", USER_USER_PAT) - .await; - - // Should only be one member, which is USER_USER_ID, and is the owner with full permissions - assert_eq!(members[0].user.id.to_string(), USER_USER_ID); - assert_eq!( - members[0].organization_permissions, - Some(OrganizationPermissions::all()) - ); - assert_eq!(members[0].role, "Owner"); - - test_env.cleanup().await; + + // Should only be one member, which is USER_USER_ID, and is the owner with full permissions + assert_eq!(members[0].user.id.to_string(), USER_USER_ID); + assert_eq!( + members[0].organization_permissions, + Some(OrganizationPermissions::all()) + ); + assert_eq!(members[0].role, "Owner"); + }) + .await; } #[actix_rt::test] async fn patch_organization() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; - - let zeta_organization_id = &test_env - .dummy - .as_ref() - .unwrap() - .organization_zeta - .organization_id; - - // Create 'theta' organization - let resp = api - .create_organization("theta", "theta_description", USER_USER_PAT) - .await; - assert_eq!(resp.status(), 200); - - // Failed patch to zeta slug: - // - slug collision with theta - // - too short slug - // - too long slug - // - not url safe slug - for title in [ - "theta", - "a", - &"a".repeat(100), - "not url safe%&^!#$##!@#$%^&*()", - ] { + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + + // Create 'theta' organization let resp = api - .edit_organization( - zeta_organization_id, - json!({ - "title": title, - "description": "theta_description" - }), - USER_USER_PAT, - ) + .create_organization("theta", "theta_description", USER_USER_PAT) .await; - assert_eq!(resp.status(), 400); - } + assert_eq!(resp.status(), 200); - // Failed patch to zeta description: - // - too short description - // - too long description - for description in ["a", &"a".repeat(300)] { + // Failed patch to zeta slug: + // - slug collision with theta + // - too short slug + // - too long slug + // - not url safe slug + for title in [ + "theta", + "a", + &"a".repeat(100), + "not url safe%&^!#$##!@#$%^&*()", + ] { + let resp = api + .edit_organization( + zeta_organization_id, + json!({ + "title": title, + "description": "theta_description" + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Failed patch to zeta description: + // - too short description + // - too long description + for description in ["a", &"a".repeat(300)] { + let resp = api + .edit_organization( + zeta_organization_id, + json!({ + "description": description + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Successful patch to many fields let resp = api .edit_organization( zeta_organization_id, json!({ - "description": description + "title": "new_title", + "description": "not url safe%&^!#$##!@#$%^&" // not-URL-safe description should still work }), USER_USER_PAT, ) .await; - assert_eq!(resp.status(), 400); - } - - // Successful patch to many fields - let resp = api - .edit_organization( - zeta_organization_id, - json!({ - "title": "new_title", - "description": "not url safe%&^!#$##!@#$%^&" // not-URL-safe description should still work - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - - // Get project using new slug - let new_title = api - .get_organization_deserialized("new_title", USER_USER_PAT) - .await; - assert_eq!(new_title.title, "new_title"); - assert_eq!(new_title.description, "not url safe%&^!#$##!@#$%^&"); - - test_env.cleanup().await; + assert_eq!(resp.status(), 204); + + // Get project using new slug + let new_title = api + .get_organization_deserialized("new_title", USER_USER_PAT) + .await; + assert_eq!(new_title.title, "new_title"); + assert_eq!(new_title.description, "not url safe%&^!#$##!@#$%^&"); + }) + .await; } // add/remove icon #[actix_rt::test] async fn add_remove_icon() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; - let zeta_organization_id = &test_env - .dummy - .as_ref() - .unwrap() - .organization_zeta - .organization_id; - - // Get project - let resp = test_env - .v3 - .get_organization_deserialized(zeta_organization_id, USER_USER_PAT) - .await; - assert_eq!(resp.icon_url, None); - - // Icon edit - // Uses alpha organization to delete this icon - let resp = api - .edit_organization_icon( - zeta_organization_id, - Some(get_icon_data(DummyImage::SmallIcon)), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - - // Get project - let zeta_org = api - .get_organization_deserialized(zeta_organization_id, USER_USER_PAT) - .await; - assert!(zeta_org.icon_url.is_some()); - - // Icon delete - // Uses alpha organization to delete added icon - let resp = api - .edit_organization_icon(zeta_organization_id, None, USER_USER_PAT) - .await; - assert_eq!(resp.status(), 204); - - // Get project - let zeta_org = api - .get_organization_deserialized(zeta_organization_id, USER_USER_PAT) - .await; - assert!(zeta_org.icon_url.is_none()); - - test_env.cleanup().await; -} + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + + // Get project + let resp = test_env + .api + .get_organization_deserialized(zeta_organization_id, USER_USER_PAT) + .await; + assert_eq!(resp.icon_url, None); -// delete org -#[actix_rt::test] -async fn delete_org() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; - let zeta_organization_id = &test_env - .dummy - .as_ref() - .unwrap() - .organization_zeta - .organization_id; - - let resp = api - .delete_organization(zeta_organization_id, USER_USER_PAT) - .await; - assert_eq!(resp.status(), 204); - - // Get organization, which should no longer exist - let resp = api - .get_organization(zeta_organization_id, USER_USER_PAT) - .await; - assert_eq!(resp.status(), 404); - - test_env.cleanup().await; -} + // Icon edit + // Uses alpha organization to delete this icon + let resp = api + .edit_organization_icon( + zeta_organization_id, + Some(get_icon_data(DummyImage::SmallIcon)), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); -// add/remove organization projects -#[actix_rt::test] -async fn add_remove_organization_projects() { - let test_env = TestEnvironment::build(None).await; - let alpha_project_id: &str = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let alpha_project_slug: &str = &test_env.dummy.as_ref().unwrap().project_alpha.project_slug; - let zeta_organization_id: &str = &test_env - .dummy - .as_ref() - .unwrap() - .organization_zeta - .organization_id; - - // Add/remove project to organization, first by ID, then by slug - for alpha in [alpha_project_id, alpha_project_slug] { - let resp = test_env - .v3 - .organization_add_project(zeta_organization_id, alpha, USER_USER_PAT) + // Get project + let zeta_org = api + .get_organization_deserialized(zeta_organization_id, USER_USER_PAT) .await; - assert_eq!(resp.status(), 200); + assert!(zeta_org.icon_url.is_some()); - // Get organization projects - let projects = test_env - .v3 - .get_organization_projects_deserialized(zeta_organization_id, USER_USER_PAT) + // Icon delete + // Uses alpha organization to delete added icon + let resp = api + .edit_organization_icon(zeta_organization_id, None, USER_USER_PAT) .await; - assert_eq!(projects[0].id.to_string(), alpha_project_id); - assert_eq!(projects[0].slug, Some(alpha_project_slug.to_string())); + assert_eq!(resp.status(), 204); - // Remove project from organization - let resp = test_env - .v3 - .organization_remove_project(zeta_organization_id, alpha, USER_USER_PAT) + // Get project + let zeta_org = api + .get_organization_deserialized(zeta_organization_id, USER_USER_PAT) .await; - assert_eq!(resp.status(), 200); + assert!(zeta_org.icon_url.is_none()); + }) + .await; +} - // Get organization projects - let projects = test_env - .v3 - .get_organization_projects_deserialized(zeta_organization_id, USER_USER_PAT) +// delete org +#[actix_rt::test] +async fn delete_org() { + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + + let resp = api + .delete_organization(zeta_organization_id, USER_USER_PAT) .await; - assert!(projects.is_empty()); - } + assert_eq!(resp.status(), 204); - test_env.cleanup().await; + // Get organization, which should no longer exist + let resp = api + .get_organization(zeta_organization_id, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 404); + }) + .await; } +// add/remove organization projects #[actix_rt::test] -async fn permissions_patch_organization() { - let test_env = TestEnvironment::build(Some(8)).await; +async fn add_remove_organization_projects() { + with_test_environment(None, |test_env: TestEnvironment| async move { + let alpha_project_id: &str = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_project_slug: &str = &test_env.dummy.as_ref().unwrap().project_alpha.project_slug; + let zeta_organization_id: &str = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + + // Add/remove project to organization, first by ID, then by slug + for alpha in [alpha_project_id, alpha_project_slug] { + let resp = test_env + .api + .organization_add_project(zeta_organization_id, alpha, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 200); + + // Get organization projects + let projects = test_env + .api + .get_organization_projects_deserialized(zeta_organization_id, USER_USER_PAT) + .await; + assert_eq!(projects[0].id.to_string(), alpha_project_id); + assert_eq!(projects[0].slug, Some(alpha_project_slug.to_string())); + + // Remove project from organization + let resp = test_env + .api + .organization_remove_project(zeta_organization_id, alpha, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 200); + + // Get organization projects + let projects = test_env + .api + .get_organization_projects_deserialized(zeta_organization_id, USER_USER_PAT) + .await; + assert!(projects.is_empty()); + } + }) + .await; +} - // For each permission covered by EDIT_DETAILS, ensure the permission is required - let edit_details = OrganizationPermissions::EDIT_DETAILS; - let test_pairs = [ - ("title", json!("")), // generated in the test to not collide slugs - ("description", json!("New description")), - ]; +#[actix_rt::test] +async fn permissions_patch_organization() { + with_test_environment_all(None, |test_env| async move { + // For each permission covered by EDIT_DETAILS, ensure the permission is required + let edit_details = OrganizationPermissions::EDIT_DETAILS; + let test_pairs = [ + ("title", json!("")), // generated in the test to not collide slugs + ("description", json!("New description")), + ]; + + for (key, value) in test_pairs { + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!( + "/v3/organization/{}", + ctx.organization_id.unwrap() + )) + .set_json(json!({ + key: if key == "title" { + json!(generate_random_name("randomslug")) + } else { + value.clone() + }, + })) + }; + PermissionsTest::new(&test_env) + .simple_organization_permissions_test(edit_details, req_gen) + .await + .unwrap(); + } + }) + .await; +} - for (key, value) in test_pairs { +// Not covered by PATCH /organization +#[actix_rt::test] +async fn permissions_edit_details() { + with_test_environment_all(None, |test_env| async move { + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + let edit_details = OrganizationPermissions::EDIT_DETAILS; + + // Icon edit + // Uses alpha organization to delete this icon let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() .uri(&format!( - "/v3/organization/{}", + "/v3/organization/{}/icon?ext=png", ctx.organization_id.unwrap() )) - .set_json(json!({ - key: if key == "title" { - json!(generate_random_name("randomslug")) - } else { - value.clone() - }, - })) + .set_payload(Bytes::from( + include_bytes!("../tests/files/200x200.png") as &[u8] + )) }; PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) .simple_organization_permissions_test(edit_details, req_gen) .await .unwrap(); - } - - test_env.cleanup().await; -} -// Not covered by PATCH /organization -#[actix_rt::test] -async fn permissions_edit_details() { - let test_env = TestEnvironment::build(None).await; - - let zeta_organization_id = &test_env - .dummy - .as_ref() - .unwrap() - .organization_zeta - .organization_id; - let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; - - let edit_details = OrganizationPermissions::EDIT_DETAILS; - - // Icon edit - // Uses alpha organization to delete this icon - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( + // Icon delete + // Uses alpha project to delete added icon + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( "/v3/organization/{}/icon?ext=png", ctx.organization_id.unwrap() )) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] - )) - }; - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_organization_permissions_test(edit_details, req_gen) - .await - .unwrap(); - - // Icon delete - // Uses alpha project to delete added icon - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/organization/{}/icon?ext=png", - ctx.organization_id.unwrap() - )) - }; - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_organization_permissions_test(edit_details, req_gen) - .await - .unwrap(); + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(edit_details, req_gen) + .await + .unwrap(); + }) + .await; } #[actix_rt::test] async fn permissions_manage_invites() { // Add member, remove member, edit member - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; - - let zeta_organization_id = &test_env - .dummy - .as_ref() - .unwrap() - .organization_zeta - .organization_id; - let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; - - let manage_invites = OrganizationPermissions::MANAGE_INVITES; - - // Add member - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!("/v3/team/{}/members", ctx.team_id.unwrap())) - .set_json(json!({ - "user_id": MOD_USER_ID, - "permissions": 0, - "organization_permissions": 0, - })) - }; - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_organization_permissions_test(manage_invites, req_gen) - .await - .unwrap(); - - // Edit member - let edit_member = OrganizationPermissions::EDIT_MEMBER; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( + with_test_environment_all(None, |test_env| async move { + let api = &test_env.api; + + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + let manage_invites = OrganizationPermissions::MANAGE_INVITES; + + // Add member + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::post() + .uri(&format!("/v3/team/{}/members", ctx.team_id.unwrap())) + .set_json(json!({ + "user_id": MOD_USER_ID, + "permissions": 0, + "organization_permissions": 0, + })) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(manage_invites, req_gen) + .await + .unwrap(); + + // Edit member + let edit_member = OrganizationPermissions::EDIT_MEMBER; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!( + "/v3/team/{}/members/{MOD_USER_ID}", + ctx.team_id.unwrap() + )) + .set_json(json!({ + "organization_permissions": 0, + })) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(edit_member, req_gen) + .await + .unwrap(); + + // remove member + // requires manage_invites if they have not yet accepted the invite + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( "/v3/team/{}/members/{MOD_USER_ID}", ctx.team_id.unwrap() )) - .set_json(json!({ - "organization_permissions": 0, - })) - }; - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_organization_permissions_test(edit_member, req_gen) - .await - .unwrap(); - - // remove member - // requires manage_invites if they have not yet accepted the invite - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/team/{}/members/{MOD_USER_ID}", - ctx.team_id.unwrap() - )) - }; - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_organization_permissions_test(manage_invites, req_gen) - .await - .unwrap(); - - // re-add member for testing - let resp = api - .add_user_to_team(zeta_team_id, MOD_USER_ID, None, None, ADMIN_USER_PAT) - .await; - assert_eq!(resp.status(), 204); - let resp = api.join_team(zeta_team_id, MOD_USER_PAT).await; - assert_eq!(resp.status(), 204); - - // remove existing member (requires remove_member) - let remove_member = OrganizationPermissions::REMOVE_MEMBER; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/team/{}/members/{MOD_USER_ID}", - ctx.team_id.unwrap() - )) - }; - - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_organization_permissions_test(remove_member, req_gen) - .await - .unwrap(); - - test_env.cleanup().await; + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(manage_invites, req_gen) + .await + .unwrap(); + + // re-add member for testing + let resp = api + .add_user_to_team(zeta_team_id, MOD_USER_ID, None, None, ADMIN_USER_PAT) + .await; + assert_eq!(resp.status(), 204); + let resp = api.join_team(zeta_team_id, MOD_USER_PAT).await; + assert_eq!(resp.status(), 204); + + // remove existing member (requires remove_member) + let remove_member = OrganizationPermissions::REMOVE_MEMBER; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( + "/v3/team/{}/members/{MOD_USER_ID}", + ctx.team_id.unwrap() + )) + }; + + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(remove_member, req_gen) + .await + .unwrap(); + }) + .await; } #[actix_rt::test] async fn permissions_add_remove_project() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; - - let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; - let zeta_organization_id = &test_env - .dummy - .as_ref() - .unwrap() - .organization_zeta - .organization_id; - let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; - - let add_project = OrganizationPermissions::ADD_PROJECT; - - // First, we add FRIEND_USER_ID to the alpha project and transfer ownership to them - // This is because the ownership of a project is needed to add it to an organization - let resp = api - .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) - .await; - assert_eq!(resp.status(), 204); - let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await; - assert_eq!(resp.status(), 204); - let resp = api - .transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) - .await; - assert_eq!(resp.status(), 204); - - // Now, FRIEND_USER_ID owns the alpha project - // Add alpha project to zeta organization - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!( - "/v3/organization/{}/projects", + with_test_environment_all(None, |test_env| async move { + let api = &test_env.api; + + let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + let add_project = OrganizationPermissions::ADD_PROJECT; + + // First, we add FRIEND_USER_ID to the alpha project and transfer ownership to them + // This is because the ownership of a project is needed to add it to an organization + let resp = api + .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 204); + let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await; + assert_eq!(resp.status(), 204); + let resp = api + .transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 204); + + // Now, FRIEND_USER_ID owns the alpha project + // Add alpha project to zeta organization + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::post() + .uri(&format!( + "/v3/organization/{}/projects", + ctx.organization_id.unwrap() + )) + .set_json(json!({ + "project_id": alpha_project_id, + })) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(add_project, req_gen) + .await + .unwrap(); + + // Remove alpha project from zeta organization + let remove_project = OrganizationPermissions::REMOVE_PROJECT; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( + "/v3/organization/{}/projects/{alpha_project_id}", ctx.organization_id.unwrap() )) - .set_json(json!({ - "project_id": alpha_project_id, - })) - }; - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_organization_permissions_test(add_project, req_gen) - .await - .unwrap(); - - // Remove alpha project from zeta organization - let remove_project = OrganizationPermissions::REMOVE_PROJECT; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/organization/{}/projects/{alpha_project_id}", - ctx.organization_id.unwrap() - )) - }; - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_organization_permissions_test(remove_project, req_gen) - .await - .unwrap(); - - test_env.cleanup().await; + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(remove_project, req_gen) + .await + .unwrap(); + }) + .await; } #[actix_rt::test] async fn permissions_delete_organization() { - let test_env = TestEnvironment::build(None).await; - let delete_organization = OrganizationPermissions::DELETE_ORGANIZATION; - - // Now, FRIEND_USER_ID owns the alpha project - // Add alpha project to zeta organization - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/organization/{}", - ctx.organization_id.unwrap() - )) - }; - PermissionsTest::new(&test_env) - .simple_organization_permissions_test(delete_organization, req_gen) - .await - .unwrap(); - - test_env.cleanup().await; + with_test_environment_all(None, |test_env| async move { + let delete_organization = OrganizationPermissions::DELETE_ORGANIZATION; + + // Now, FRIEND_USER_ID owns the alpha project + // Add alpha project to zeta organization + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( + "/v3/organization/{}", + ctx.organization_id.unwrap() + )) + }; + PermissionsTest::new(&test_env) + .simple_organization_permissions_test(delete_organization, req_gen) + .await + .unwrap(); + }) + .await; } #[actix_rt::test] async fn permissions_add_default_project_permissions() { - let test_env = TestEnvironment::build(None).await; - let zeta_organization_id = &test_env - .dummy - .as_ref() - .unwrap() - .organization_zeta - .organization_id; - let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; - - // Add member - let add_member_default_permissions = OrganizationPermissions::MANAGE_INVITES - | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS; - - // Failure test should include MANAGE_INVITES, as it is required to add - // default permissions on an invited user, but should still fail without EDIT_MEMBER_DEFAULT_PERMISSIONS - let failure_with_add_member = (OrganizationPermissions::all() ^ add_member_default_permissions) - | OrganizationPermissions::MANAGE_INVITES; - - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!("/v3/team/{}/members", ctx.team_id.unwrap())) - .set_json(json!({ - "user_id": MOD_USER_ID, - "permissions": (ProjectPermissions::UPLOAD_VERSION | ProjectPermissions::DELETE_VERSION).bits(), - "organization_permissions": 0, - })) - }; - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .with_failure_permissions(None, Some(failure_with_add_member)) - .simple_organization_permissions_test(add_member_default_permissions, req_gen) - .await - .unwrap(); - - // Now that member is added, modify default permissions - let modify_member_default_permission = OrganizationPermissions::EDIT_MEMBER - | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS; - - // Failure test should include MANAGE_INVITES, as it is required to add - // default permissions on an invited user, but should still fail without EDIT_MEMBER_DEFAULT_PERMISSIONS - let failure_with_modify_member = (OrganizationPermissions::all() - ^ add_member_default_permissions) - | OrganizationPermissions::EDIT_MEMBER; - - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( - "/v3/team/{}/members/{MOD_USER_ID}", - ctx.team_id.unwrap() - )) - .set_json(json!({ - "permissions": ProjectPermissions::EDIT_DETAILS.bits(), - })) - }; - PermissionsTest::new(&test_env) - .with_existing_organization(zeta_organization_id, zeta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .with_failure_permissions(None, Some(failure_with_modify_member)) - .simple_organization_permissions_test(modify_member_default_permission, req_gen) - .await - .unwrap(); - - test_env.cleanup().await; + with_test_environment_all(None, |test_env| async move { + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + // Add member + let add_member_default_permissions = OrganizationPermissions::MANAGE_INVITES + | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS; + + // Failure test should include MANAGE_INVITES, as it is required to add + // default permissions on an invited user, but should still fail without EDIT_MEMBER_DEFAULT_PERMISSIONS + let failure_with_add_member = (OrganizationPermissions::all() ^ add_member_default_permissions) + | OrganizationPermissions::MANAGE_INVITES; + + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::post() + .uri(&format!("/v3/team/{}/members", ctx.team_id.unwrap())) + .set_json(json!({ + "user_id": MOD_USER_ID, + "permissions": (ProjectPermissions::UPLOAD_VERSION | ProjectPermissions::DELETE_VERSION).bits(), + "organization_permissions": 0, + })) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .with_failure_permissions(None, Some(failure_with_add_member)) + .simple_organization_permissions_test(add_member_default_permissions, req_gen) + .await + .unwrap(); + + // Now that member is added, modify default permissions + let modify_member_default_permission = OrganizationPermissions::EDIT_MEMBER + | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS; + + // Failure test should include MANAGE_INVITES, as it is required to add + // default permissions on an invited user, but should still fail without EDIT_MEMBER_DEFAULT_PERMISSIONS + let failure_with_modify_member = (OrganizationPermissions::all() + ^ add_member_default_permissions) + | OrganizationPermissions::EDIT_MEMBER; + + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!( + "/v3/team/{}/members/{MOD_USER_ID}", + ctx.team_id.unwrap() + )) + .set_json(json!({ + "permissions": ProjectPermissions::EDIT_DETAILS.bits(), + })) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .with_failure_permissions(None, Some(failure_with_modify_member)) + .simple_organization_permissions_test(modify_member_default_permission, req_gen) + .await + .unwrap(); + + }).await; } #[actix_rt::test] async fn permissions_organization_permissions_consistency_test() { - let test_env = TestEnvironment::build(None).await; - // Ensuring that permission are as we expect them to be - // Full organization permissions test - let success_permissions = OrganizationPermissions::EDIT_DETAILS; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( - "/v3/organization/{}", - ctx.organization_id.unwrap() - )) - .set_json(json!({ - "description": "Example description - changed.", - })) - }; - PermissionsTest::new(&test_env) - .full_organization_permissions_tests(success_permissions, req_gen) - .await - .unwrap(); - - test_env.cleanup().await; + with_test_environment_all(None, |test_env| async move { + // Ensuring that permission are as we expect them to be + // Full organization permissions test + let success_permissions = OrganizationPermissions::EDIT_DETAILS; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!( + "/v3/organization/{}", + ctx.organization_id.unwrap() + )) + .set_json(json!({ + "description": "Example description - changed.", + })) + }; + PermissionsTest::new(&test_env) + .full_organization_permissions_tests(success_permissions, req_gen) + .await + .unwrap(); + }) + .await; } diff --git a/tests/pats.rs b/tests/pats.rs index a46b4e63..570a072f 100644 --- a/tests/pats.rs +++ b/tests/pats.rs @@ -1,7 +1,7 @@ use actix_web::test; use chrono::{Duration, Utc}; -use common::database::*; -use common::environment::TestEnvironment; +use common::{database::*, environment::with_test_environment_all}; + use labrinth::models::pats::Scopes; use serde_json::json; @@ -16,275 +16,271 @@ mod common; // - ensure PATs can be deleted #[actix_rt::test] pub async fn pat_full_test() { - let test_env = TestEnvironment::build(None).await; - - // Create a PAT for a full test - let req = test::TestRequest::post() - .uri("/v3/pat") - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example - "name": "test_pat_scopes Test", - "expires": Utc::now() + Duration::days(1), - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status().as_u16(), 200); - let success: serde_json::Value = test::read_body_json(resp).await; - let id = success["id"].as_str().unwrap(); - - // Has access token and correct scopes - assert!(success["access_token"].as_str().is_some()); - assert_eq!( - success["scopes"].as_u64().unwrap(), - Scopes::COLLECTION_CREATE.bits() - ); - let access_token = success["access_token"].as_str().unwrap(); + with_test_environment_all(None, |test_env| async move { + // Create a PAT for a full test + let req = test::TestRequest::post() + .uri("/v3/pat") + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example + "name": "test_pat_scopes Test", + "expires": Utc::now() + Duration::days(1), + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status().as_u16(), 200); + let success: serde_json::Value = test::read_body_json(resp).await; + let id = success["id"].as_str().unwrap(); - // Get PAT again - let req = test::TestRequest::get() - .append_header(("Authorization", USER_USER_PAT)) - .uri("/v3/pat") - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status().as_u16(), 200); - let success: serde_json::Value = test::read_body_json(resp).await; + // Has access token and correct scopes + assert!(success["access_token"].as_str().is_some()); + assert_eq!( + success["scopes"].as_u64().unwrap(), + Scopes::COLLECTION_CREATE.bits() + ); + let access_token = success["access_token"].as_str().unwrap(); - // Ensure access token is NOT returned for any PATs - for pat in success.as_array().unwrap() { - assert!(pat["access_token"].as_str().is_none()); - } + // Get PAT again + let req = test::TestRequest::get() + .append_header(("Authorization", USER_USER_PAT)) + .uri("/v3/pat") + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status().as_u16(), 200); + let success: serde_json::Value = test::read_body_json(resp).await; - // Create mock test for using PAT - let mock_pat_test = |token: &str| { - let token = token.to_string(); - async { - let req = test::TestRequest::post() - .uri("/v3/collection") - .append_header(("Authorization", token)) - .set_json(json!({ - "title": "Test Collection 1", - "description": "Test Collection Description" - })) - .to_request(); - let resp = test_env.call(req).await; - resp.status().as_u16() + // Ensure access token is NOT returned for any PATs + for pat in success.as_array().unwrap() { + assert!(pat["access_token"].as_str().is_none()); } - }; - - assert_eq!(mock_pat_test(access_token).await, 200); - // Change scopes and test again - let req = test::TestRequest::patch() - .uri(&format!("/v3/pat/{}", id)) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "scopes": 0, - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status().as_u16(), 204); - assert_eq!(mock_pat_test(access_token).await, 401); // No longer works + // Create mock test for using PAT + let mock_pat_test = |token: &str| { + let token = token.to_string(); + async { + let req = test::TestRequest::post() + .uri("/v3/collection") + .append_header(("Authorization", token)) + .set_json(json!({ + "title": "Test Collection 1", + "description": "Test Collection Description" + })) + .to_request(); + let resp = test_env.call(req).await; + resp.status().as_u16() + } + }; - // Change scopes back, and set expiry to the past, and test again - let req = test::TestRequest::patch() - .uri(&format!("/v3/pat/{}", id)) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "scopes": Scopes::COLLECTION_CREATE, - "expires": Utc::now() + Duration::seconds(1), // expires in 1 second - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status().as_u16(), 204); + assert_eq!(mock_pat_test(access_token).await, 200); - // Wait 1 second before testing again for expiry - tokio::time::sleep(Duration::seconds(1).to_std().unwrap()).await; - assert_eq!(mock_pat_test(access_token).await, 401); // No longer works + // Change scopes and test again + let req = test::TestRequest::patch() + .uri(&format!("/v3/pat/{}", id)) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "scopes": 0, + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status().as_u16(), 204); + assert_eq!(mock_pat_test(access_token).await, 401); // No longer works - // Change everything back to normal and test again - let req = test::TestRequest::patch() - .uri(&format!("/v3/pat/{}", id)) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "expires": Utc::now() + Duration::days(1), // no longer expired! - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status().as_u16(), 204); - assert_eq!(mock_pat_test(access_token).await, 200); // Works again + // Change scopes back, and set expiry to the past, and test again + let req = test::TestRequest::patch() + .uri(&format!("/v3/pat/{}", id)) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "scopes": Scopes::COLLECTION_CREATE, + "expires": Utc::now() + Duration::seconds(1), // expires in 1 second + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status().as_u16(), 204); - // Patching to a bad expiry should fail - let req = test::TestRequest::patch() - .uri(&format!("/v3/pat/{}", id)) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "expires": Utc::now() - Duration::days(1), // Past - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status().as_u16(), 400); + // Wait 1 second before testing again for expiry + tokio::time::sleep(Duration::seconds(1).to_std().unwrap()).await; + assert_eq!(mock_pat_test(access_token).await, 401); // No longer works - // Similar to above with PAT creation, patching to a bad scope should fail - for i in 0..64 { - let scope = Scopes::from_bits_truncate(1 << i); - if !Scopes::all().contains(scope) { - continue; - } + // Change everything back to normal and test again + let req = test::TestRequest::patch() + .uri(&format!("/v3/pat/{}", id)) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "expires": Utc::now() + Duration::days(1), // no longer expired! + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status().as_u16(), 204); + assert_eq!(mock_pat_test(access_token).await, 200); // Works again + // Patching to a bad expiry should fail let req = test::TestRequest::patch() .uri(&format!("/v3/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ - "scopes": scope.bits(), + "expires": Utc::now() - Duration::days(1), // Past })) .to_request(); let resp = test_env.call(req).await; - assert_eq!( - resp.status().as_u16(), - if scope.is_restricted() { 400 } else { 204 } - ); - } + assert_eq!(resp.status().as_u16(), 400); + + // Similar to above with PAT creation, patching to a bad scope should fail + for i in 0..64 { + let scope = Scopes::from_bits_truncate(1 << i); + if !Scopes::all().contains(scope) { + continue; + } - // Delete PAT - let req = test::TestRequest::delete() - .append_header(("Authorization", USER_USER_PAT)) - .uri(&format!("/v3/pat/{}", id)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status().as_u16(), 204); + let req = test::TestRequest::patch() + .uri(&format!("/v3/pat/{}", id)) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "scopes": scope.bits(), + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!( + resp.status().as_u16(), + if scope.is_restricted() { 400 } else { 204 } + ); + } - // Cleanup test db - test_env.cleanup().await; + // Delete PAT + let req = test::TestRequest::delete() + .append_header(("Authorization", USER_USER_PAT)) + .uri(&format!("/v3/pat/{}", id)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status().as_u16(), 204); + }) + .await; } // Test illegal PAT setting, both in POST and PATCH #[actix_rt::test] pub async fn bad_pats() { - let test_env = TestEnvironment::build(None).await; - - // Creating a PAT with no name should fail - let req = test::TestRequest::post() - .uri("/v3/pat") - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example - "expires": Utc::now() + Duration::days(1), - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status().as_u16(), 400); - - // Name too short or too long should fail - for name in ["n", "this_name_is_too_long".repeat(16).as_str()] { + with_test_environment_all(None, |test_env| async move { + // Creating a PAT with no name should fail let req = test::TestRequest::post() .uri("/v3/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ - "name": name, "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example "expires": Utc::now() + Duration::days(1), })) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status().as_u16(), 400); - } - - // Creating a PAT with an expiry in the past should fail - let req = test::TestRequest::post() - .uri("/v3/pat") - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example - "name": "test_pat_scopes Test", - "expires": Utc::now() - Duration::days(1), - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status().as_u16(), 400); - // Make a PAT with each scope, with the result varying by whether that scope is restricted - for i in 0..64 { - let scope = Scopes::from_bits_truncate(1 << i); - if !Scopes::all().contains(scope) { - continue; + // Name too short or too long should fail + for name in ["n", "this_name_is_too_long".repeat(16).as_str()] { + let req = test::TestRequest::post() + .uri("/v3/pat") + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "name": name, + "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example + "expires": Utc::now() + Duration::days(1), + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status().as_u16(), 400); } + + // Creating a PAT with an expiry in the past should fail let req = test::TestRequest::post() .uri("/v3/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ - "scopes": scope.bits(), - "name": format!("test_pat_scopes Name {}", i), - "expires": Utc::now() + Duration::days(1), + "scopes": Scopes::COLLECTION_CREATE, // Collection create as an easily tested example + "name": "test_pat_scopes Test", + "expires": Utc::now() - Duration::days(1), })) .to_request(); let resp = test_env.call(req).await; - assert_eq!( - resp.status().as_u16(), - if scope.is_restricted() { 400 } else { 200 } - ); - } + assert_eq!(resp.status().as_u16(), 400); - // Create a 'good' PAT for patching - let req = test::TestRequest::post() - .uri("/v3/pat") - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "scopes": Scopes::COLLECTION_CREATE, - "name": "test_pat_scopes Test", - "expires": Utc::now() + Duration::days(1), - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status().as_u16(), 200); - let success: serde_json::Value = test::read_body_json(resp).await; - let id = success["id"].as_str().unwrap(); + // Make a PAT with each scope, with the result varying by whether that scope is restricted + for i in 0..64 { + let scope = Scopes::from_bits_truncate(1 << i); + if !Scopes::all().contains(scope) { + continue; + } + let req = test::TestRequest::post() + .uri("/v3/pat") + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "scopes": scope.bits(), + "name": format!("test_pat_scopes Name {}", i), + "expires": Utc::now() + Duration::days(1), + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!( + resp.status().as_u16(), + if scope.is_restricted() { 400 } else { 200 } + ); + } - // Patching to a bad name should fail - for name in ["n", "this_name_is_too_long".repeat(16).as_str()] { + // Create a 'good' PAT for patching let req = test::TestRequest::post() .uri("/v3/pat") .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ - "name": name, + "scopes": Scopes::COLLECTION_CREATE, + "name": "test_pat_scopes Test", + "expires": Utc::now() + Duration::days(1), })) .to_request(); let resp = test_env.call(req).await; - assert_eq!(resp.status().as_u16(), 400); - } + assert_eq!(resp.status().as_u16(), 200); + let success: serde_json::Value = test::read_body_json(resp).await; + let id = success["id"].as_str().unwrap(); - // Patching to a bad expiry should fail - let req = test::TestRequest::patch() - .uri(&format!("/v3/pat/{}", id)) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "expires": Utc::now() - Duration::days(1), // Past - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status().as_u16(), 400); - - // Similar to above with PAT creation, patching to a bad scope should fail - for i in 0..64 { - let scope = Scopes::from_bits_truncate(1 << i); - if !Scopes::all().contains(scope) { - continue; + // Patching to a bad name should fail + for name in ["n", "this_name_is_too_long".repeat(16).as_str()] { + let req = test::TestRequest::post() + .uri("/v3/pat") + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "name": name, + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status().as_u16(), 400); } + // Patching to a bad expiry should fail let req = test::TestRequest::patch() .uri(&format!("/v3/pat/{}", id)) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ - "scopes": scope.bits(), + "expires": Utc::now() - Duration::days(1), // Past })) .to_request(); let resp = test_env.call(req).await; - assert_eq!( - resp.status().as_u16(), - if scope.is_restricted() { 400 } else { 204 } - ); - } + assert_eq!(resp.status().as_u16(), 400); - // Cleanup test db - test_env.cleanup().await; + // Similar to above with PAT creation, patching to a bad scope should fail + for i in 0..64 { + let scope = Scopes::from_bits_truncate(1 << i); + if !Scopes::all().contains(scope) { + continue; + } + + let req = test::TestRequest::patch() + .uri(&format!("/v3/pat/{}", id)) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "scopes": scope.bits(), + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!( + resp.status().as_u16(), + if scope.is_restricted() { 400 } else { 204 } + ); + } + }) + .await; } diff --git a/tests/project.rs b/tests/project.rs index 40c9cd30..0c71bd56 100644 --- a/tests/project.rs +++ b/tests/project.rs @@ -4,7 +4,8 @@ use bytes::Bytes; use chrono::{Duration, Utc}; use common::database::*; use common::dummy_data::DUMMY_CATEGORIES; -use common::environment::{with_test_environment, TestEnvironment}; + +use common::environment::with_test_environment_all; use common::permissions::{PermissionsTest, PermissionsTestContext}; use futures::StreamExt; use labrinth::database::models::project_item::{PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE}; @@ -13,440 +14,449 @@ use labrinth::models::teams::ProjectPermissions; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; use serde_json::json; +use crate::common::api_common::{ApiProject, ApiVersion}; + mod common; #[actix_rt::test] async fn test_get_project() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let beta_project_id = &test_env.dummy.as_ref().unwrap().project_beta.project_id; - let alpha_project_slug = &test_env.dummy.as_ref().unwrap().project_alpha.project_slug; - let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; - - // Perform request on dummy data - let req = test::TestRequest::get() - .uri(&format!("/v3/project/{alpha_project_id}")) - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - let status = resp.status(); - let body: serde_json::Value = test::read_body_json(resp).await; - - assert_eq!(status, 200); - assert_eq!(body["id"], json!(alpha_project_id)); - assert_eq!(body["slug"], json!(alpha_project_slug)); - let versions = body["versions"].as_array().unwrap(); - assert_eq!(versions[0], json!(alpha_version_id)); - - // Confirm that the request was cached - let mut redis_pool = test_env.db.redis_pool.connect().await.unwrap(); - assert_eq!( - redis_pool - .get(PROJECTS_SLUGS_NAMESPACE, alpha_project_slug) + with_test_environment_all(None, |test_env| async move { + let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let beta_project_id = &test_env.dummy.as_ref().unwrap().project_beta.project_id; + let alpha_project_slug = &test_env.dummy.as_ref().unwrap().project_alpha.project_slug; + let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; + + // Perform request on dummy data + let req = test::TestRequest::get() + .uri(&format!("/v3/project/{alpha_project_id}")) + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + let status = resp.status(); + let body: serde_json::Value = test::read_body_json(resp).await; + + assert_eq!(status, 200); + assert_eq!(body["id"], json!(alpha_project_id)); + assert_eq!(body["slug"], json!(alpha_project_slug)); + let versions = body["versions"].as_array().unwrap(); + assert_eq!(versions[0], json!(alpha_version_id)); + + // Confirm that the request was cached + let mut redis_pool = test_env.db.redis_pool.connect().await.unwrap(); + assert_eq!( + redis_pool + .get(PROJECTS_SLUGS_NAMESPACE, alpha_project_slug) + .await + .unwrap() + .and_then(|x| x.parse::().ok()), + Some(parse_base62(alpha_project_id).unwrap() as i64) + ); + + let cached_project = redis_pool + .get( + PROJECTS_NAMESPACE, + &parse_base62(alpha_project_id).unwrap().to_string(), + ) .await .unwrap() - .and_then(|x| x.parse::().ok()), - Some(parse_base62(alpha_project_id).unwrap() as i64) - ); - - let cached_project = redis_pool - .get( - PROJECTS_NAMESPACE, - &parse_base62(alpha_project_id).unwrap().to_string(), - ) - .await - .unwrap() - .unwrap(); - let cached_project: serde_json::Value = serde_json::from_str(&cached_project).unwrap(); - assert_eq!(cached_project["inner"]["slug"], json!(alpha_project_slug)); - - // Make the request again, this time it should be cached - let req = test::TestRequest::get() - .uri(&format!("/v3/project/{alpha_project_id}")) - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - let status = resp.status(); - assert_eq!(status, 200); - - let body: serde_json::Value = test::read_body_json(resp).await; - assert_eq!(body["id"], json!(alpha_project_id)); - assert_eq!(body["slug"], json!(alpha_project_slug)); - - // Request should fail on non-existent project - let req = test::TestRequest::get() - .uri("/v3/project/nonexistent") - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 404); - - // Similarly, request should fail on non-authorized user, on a yet-to-be-approved or hidden project, with a 404 (hiding the existence of the project) - let req = test::TestRequest::get() - .uri(&format!("/v3/project/{beta_project_id}")) - .append_header(("Authorization", ENEMY_USER_PAT)) - .to_request(); - - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 404); - - // Cleanup test db - test_env.cleanup().await; + .unwrap(); + let cached_project: serde_json::Value = serde_json::from_str(&cached_project).unwrap(); + assert_eq!(cached_project["inner"]["slug"], json!(alpha_project_slug)); + + // Make the request again, this time it should be cached + let req = test::TestRequest::get() + .uri(&format!("/v3/project/{alpha_project_id}")) + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + let status = resp.status(); + assert_eq!(status, 200); + + let body: serde_json::Value = test::read_body_json(resp).await; + assert_eq!(body["id"], json!(alpha_project_id)); + assert_eq!(body["slug"], json!(alpha_project_slug)); + + // Request should fail on non-existent project + let req = test::TestRequest::get() + .uri("/v3/project/nonexistent") + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 404); + + // Similarly, request should fail on non-authorized user, on a yet-to-be-approved or hidden project, with a 404 (hiding the existence of the project) + let req = test::TestRequest::get() + .uri(&format!("/v3/project/{beta_project_id}")) + .append_header(("Authorization", ENEMY_USER_PAT)) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 404); + }) + .await; } #[actix_rt::test] async fn test_add_remove_project() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; - - // Generate test project data. - let mut json_data = json!( - { - "title": "Test_Add_Project project", - "slug": "demo", - "description": "Example description.", - "body": "Example body.", - "initial_versions": [{ - "file_parts": ["basic-mod.jar"], - "version_number": "1.2.3", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "client_side": "required", - "server_side": "optional", - "release_channel": "release", - "loaders": ["fabric"], - "featured": true - }], - "categories": [], - "license_id": "MIT" - } - ); - - // Basic json - let json_segment = MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - }; - - // Basic json, with a different file - json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); - let json_diff_file_segment = MultipartSegment { - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - ..json_segment.clone() - }; - - // Basic json, with a different file, and a different slug - json_data["slug"] = json!("new_demo"); - json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); - let json_diff_slug_file_segment = MultipartSegment { - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - ..json_segment.clone() - }; - - // Basic file - let file_segment = MultipartSegment { - name: "basic-mod.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - // TODO: look at these: can be used in the reuse data - data: MultipartSegmentData::Binary(include_bytes!("../tests/files/basic-mod.jar").to_vec()), - }; - - // Differently named file, with the same content (for hash testing) - let file_diff_name_segment = MultipartSegment { - name: "basic-mod-different.jar".to_string(), - filename: Some("basic-mod-different.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary(include_bytes!("../tests/files/basic-mod.jar").to_vec()), - }; - - // Differently named file, with different content - let file_diff_name_content_segment = MultipartSegment { - name: "basic-mod-different.jar".to_string(), - filename: Some("basic-mod-different.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../tests/files/basic-mod-different.jar").to_vec(), - ), - }; - - // Add a project- simple, should work. - let req = test::TestRequest::post() - .uri("/v3/project") - .append_header(("Authorization", USER_USER_PAT)) - .set_multipart(vec![json_segment.clone(), file_segment.clone()]) - .to_request(); - let resp = test_env.call(req).await; - - let status = resp.status(); - assert_eq!(status, 200); - - // Get the project we just made, and confirm that it's correct - let project = api.get_project_deserialized("demo", USER_USER_PAT).await; - assert!(project.versions.len() == 1); - let uploaded_version_id = project.versions[0]; - - // Checks files to ensure they were uploaded and correctly identify the file - let hash = sha1::Sha1::from(include_bytes!("../tests/files/basic-mod.jar")) - .digest() - .to_string(); - let version = api - .get_version_from_hash_deserialized(&hash, "sha1", USER_USER_PAT) - .await; - assert_eq!(version.id, uploaded_version_id); - - // Reusing with a different slug and the same file should fail - // Even if that file is named differently - let req = test::TestRequest::post() - .uri("/v3/project") - .append_header(("Authorization", USER_USER_PAT)) - .set_multipart(vec![ - json_diff_slug_file_segment.clone(), // Different slug, different file name - file_diff_name_segment.clone(), // Different file name, same content - ]) - .to_request(); - - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 400); - - // Reusing with the same slug and a different file should fail - let req = test::TestRequest::post() - .uri("/v3/project") - .append_header(("Authorization", USER_USER_PAT)) - .set_multipart(vec![ - json_diff_file_segment.clone(), // Same slug, different file name - file_diff_name_content_segment.clone(), // Different file name, different content - ]) - .to_request(); - - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 400); - - // Different slug, different file should succeed - let req = test::TestRequest::post() - .uri("/v3/project") - .append_header(("Authorization", USER_USER_PAT)) - .set_multipart(vec![ - json_diff_slug_file_segment.clone(), // Different slug, different file name - file_diff_name_content_segment.clone(), // Different file name, same content - ]) - .to_request(); - - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - - // Get - let project = api.get_project_deserialized("demo", USER_USER_PAT).await; - let id = project.id.to_string(); - - // Remove the project - let resp = test_env.v3.remove_project("demo", USER_USER_PAT).await; - assert_eq!(resp.status(), 204); - - // Confirm that the project is gone from the cache - let mut redis_pool = test_env.db.redis_pool.connect().await.unwrap(); - assert_eq!( - redis_pool - .get(PROJECTS_SLUGS_NAMESPACE, "demo") - .await - .unwrap() - .and_then(|x| x.parse::().ok()), - None - ); - assert_eq!( - redis_pool - .get(PROJECTS_SLUGS_NAMESPACE, &id) - .await - .unwrap() - .and_then(|x| x.parse::().ok()), - None - ); + with_test_environment_all(None, |test_env| async move { + let api = &test_env.api; + + // Generate test project data. + let mut json_data = json!( + { + "title": "Test_Add_Project project", + "slug": "demo", + "description": "Example description.", + "body": "Example body.", + "initial_versions": [{ + "file_parts": ["basic-mod.jar"], + "version_number": "1.2.3", + "version_title": "start", + "dependencies": [], + "game_versions": ["1.20.1"] , + "client_side": "required", + "server_side": "optional", + "release_channel": "release", + "loaders": ["fabric"], + "featured": true + }], + "categories": [], + "license_id": "MIT" + } + ); - // Old slug no longer works - let resp = api.get_project("demo", USER_USER_PAT).await; - assert_eq!(resp.status(), 404); + // Basic json + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + }; + + // Basic json, with a different file + json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); + let json_diff_file_segment = MultipartSegment { + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + ..json_segment.clone() + }; + + // Basic json, with a different file, and a different slug + json_data["slug"] = json!("new_demo"); + json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); + let json_diff_slug_file_segment = MultipartSegment { + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + ..json_segment.clone() + }; + + // Basic file + let file_segment = MultipartSegment { + name: "basic-mod.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + // TODO: look at these: can be used in the reuse data + data: MultipartSegmentData::Binary( + include_bytes!("../tests/files/basic-mod.jar").to_vec(), + ), + }; + + // Differently named file, with the same content (for hash testing) + let file_diff_name_segment = MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod-different.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../tests/files/basic-mod.jar").to_vec(), + ), + }; + + // Differently named file, with different content + let file_diff_name_content_segment = MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod-different.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../tests/files/basic-mod-different.jar").to_vec(), + ), + }; + + // Add a project- simple, should work. + let req = test::TestRequest::post() + .uri("/v3/project") + .append_header(("Authorization", USER_USER_PAT)) + .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + .to_request(); + let resp = test_env.call(req).await; + + let status = resp.status(); + assert_eq!(status, 200); + + // Get the project we just made, and confirm that it's correct + let project = api + .get_project_deserialized_common("demo", USER_USER_PAT) + .await; + assert!(project.versions.len() == 1); + let uploaded_version_id = project.versions[0]; + + // Checks files to ensure they were uploaded and correctly identify the file + let hash = sha1::Sha1::from(include_bytes!("../tests/files/basic-mod.jar")) + .digest() + .to_string(); + let version = api + .get_version_from_hash_deserialized_common(&hash, "sha1", USER_USER_PAT) + .await; + assert_eq!(version.id, uploaded_version_id); + + // Reusing with a different slug and the same file should fail + // Even if that file is named differently + let req = test::TestRequest::post() + .uri("/v3/project") + .append_header(("Authorization", USER_USER_PAT)) + .set_multipart(vec![ + json_diff_slug_file_segment.clone(), // Different slug, different file name + file_diff_name_segment.clone(), // Different file name, same content + ]) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); - // Cleanup test db - test_env.cleanup().await; + // Reusing with the same slug and a different file should fail + let req = test::TestRequest::post() + .uri("/v3/project") + .append_header(("Authorization", USER_USER_PAT)) + .set_multipart(vec![ + json_diff_file_segment.clone(), // Same slug, different file name + file_diff_name_content_segment.clone(), // Different file name, different content + ]) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + + // Different slug, different file should succeed + let req = test::TestRequest::post() + .uri("/v3/project") + .append_header(("Authorization", USER_USER_PAT)) + .set_multipart(vec![ + json_diff_slug_file_segment.clone(), // Different slug, different file name + file_diff_name_content_segment.clone(), // Different file name, same content + ]) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + + // Get + let project = api + .get_project_deserialized_common("demo", USER_USER_PAT) + .await; + let id = project.id.to_string(); + + // Remove the project + let resp = test_env.api.remove_project("demo", USER_USER_PAT).await; + assert_eq!(resp.status(), 204); + + // Confirm that the project is gone from the cache + let mut redis_pool = test_env.db.redis_pool.connect().await.unwrap(); + assert_eq!( + redis_pool + .get(PROJECTS_SLUGS_NAMESPACE, "demo") + .await + .unwrap() + .and_then(|x| x.parse::().ok()), + None + ); + assert_eq!( + redis_pool + .get(PROJECTS_SLUGS_NAMESPACE, &id) + .await + .unwrap() + .and_then(|x| x.parse::().ok()), + None + ); + + // Old slug no longer works + let resp = api.get_project("demo", USER_USER_PAT).await; + assert_eq!(resp.status(), 404); + }) + .await; } #[actix_rt::test] pub async fn test_patch_project() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; - - let alpha_project_slug = &test_env.dummy.as_ref().unwrap().project_alpha.project_slug; - let beta_project_slug = &test_env.dummy.as_ref().unwrap().project_beta.project_slug; - - // First, we do some patch requests that should fail. - // Failure because the user is not authorized. - let resp = api - .edit_project( - alpha_project_slug, - json!({ - "title": "Test_Add_Project project - test 1", - }), - ENEMY_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 401); - - // Failure because we are setting URL fields to invalid urls. - for url_type in ["issues_url", "source_url", "wiki_url", "discord_url"] { - let resp = api - .edit_project( - alpha_project_slug, - json!({ - url_type: "w.fake.url", - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 400); - } + with_test_environment_all(None, |test_env| async move { + let api = &test_env.api; - // Failure because these are illegal requested statuses for a normal user. - for req in ["unknown", "processing", "withheld", "scheduled"] { + let alpha_project_slug = &test_env.dummy.as_ref().unwrap().project_alpha.project_slug; + let beta_project_slug = &test_env.dummy.as_ref().unwrap().project_beta.project_slug; + + // First, we do some patch requests that should fail. + // Failure because the user is not authorized. let resp = api .edit_project( alpha_project_slug, json!({ - "requested_status": req, + "title": "Test_Add_Project project - test 1", }), - USER_USER_PAT, + ENEMY_USER_PAT, ) .await; - assert_eq!(resp.status(), 400); - } + assert_eq!(resp.status(), 401); + + // Failure because we are setting URL fields to invalid urls. + for url_type in ["issues_url", "source_url", "wiki_url", "discord_url"] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + url_type: "w.fake.url", + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Failure because these are illegal requested statuses for a normal user. + for req in ["unknown", "processing", "withheld", "scheduled"] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + "requested_status": req, + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Failure because these should not be able to be set by a non-mod + for key in ["moderation_message", "moderation_message_body"] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + key: "test", + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 401); + + // (should work for a mod, though) + let resp = api + .edit_project( + alpha_project_slug, + json!({ + key: "test", + }), + MOD_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + } + + // Failed patch to alpha slug: + // - slug collision with beta + // - too short slug + // - too long slug + // - not url safe slug + // - not url safe slug + for slug in [ + beta_project_slug, + "a", + &"a".repeat(100), + "not url safe%&^!#$##!@#$%^&*()", + ] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + "slug": slug, // the other dummy project has this slug + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } - // Failure because these should not be able to be set by a non-mod - for key in ["moderation_message", "moderation_message_body"] { + // Not allowed to directly set status, as 'beta_project_slug' (the other project) is "processing" and cannot have its status changed like this. let resp = api .edit_project( - alpha_project_slug, + beta_project_slug, json!({ - key: "test", + "status": "private" }), USER_USER_PAT, ) .await; assert_eq!(resp.status(), 401); - // (should work for a mod, though) + // Sucessful request to patch many fields. let resp = api .edit_project( alpha_project_slug, json!({ - key: "test", + "slug": "newslug", + "title": "New successful title", + "description": "New successful description", + "body": "New successful body", + "categories": [DUMMY_CATEGORIES[0]], + "license_id": "MIT", + "issues_url": "https://github.com", + "discord_url": "https://discord.gg", + "wiki_url": "https://wiki.com", + "donation_urls": [{ + "id": "patreon", + "platform": "Patreon", + "url": "https://patreon.com" + }] }), - MOD_USER_PAT, + USER_USER_PAT, ) .await; assert_eq!(resp.status(), 204); - } - - // Failed patch to alpha slug: - // - slug collision with beta - // - too short slug - // - too long slug - // - not url safe slug - // - not url safe slug - for slug in [ - beta_project_slug, - "a", - &"a".repeat(100), - "not url safe%&^!#$##!@#$%^&*()", - ] { - let resp = api - .edit_project( - alpha_project_slug, - json!({ - "slug": slug, // the other dummy project has this slug - }), - USER_USER_PAT, - ) + + // Old slug no longer works + let resp = api.get_project(alpha_project_slug, USER_USER_PAT).await; + assert_eq!(resp.status(), 404); + + // New slug does work + let project = api + .get_project_deserialized_common("newslug", USER_USER_PAT) .await; - assert_eq!(resp.status(), 400); - } - // Not allowed to directly set status, as 'beta_project_slug' (the other project) is "processing" and cannot have its status changed like this. - let resp = api - .edit_project( - beta_project_slug, - json!({ - "status": "private" - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 401); - - // Sucessful request to patch many fields. - let resp = api - .edit_project( - alpha_project_slug, - json!({ - "slug": "newslug", - "title": "New successful title", - "description": "New successful description", - "body": "New successful body", - "categories": [DUMMY_CATEGORIES[0]], - "license_id": "MIT", - "issues_url": "https://github.com", - "discord_url": "https://discord.gg", - "wiki_url": "https://wiki.com", - "donation_urls": [{ - "id": "patreon", - "platform": "Patreon", - "url": "https://patreon.com" - }] - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - - // Old slug no longer works - let resp = api.get_project(alpha_project_slug, USER_USER_PAT).await; - assert_eq!(resp.status(), 404); - - // New slug does work - let project = api.get_project_deserialized("newslug", USER_USER_PAT).await; - - assert_eq!(project.slug.unwrap(), "newslug"); - assert_eq!(project.title, "New successful title"); - assert_eq!(project.description, "New successful description"); - assert_eq!(project.body, "New successful body"); - assert_eq!(project.categories, vec![DUMMY_CATEGORIES[0]]); - assert_eq!(project.license.id, "MIT"); - assert_eq!(project.issues_url, Some("https://github.com".to_string())); - assert_eq!(project.discord_url, Some("https://discord.gg".to_string())); - assert_eq!(project.wiki_url, Some("https://wiki.com".to_string())); - assert_eq!(project.donation_urls.unwrap()[0].url, "https://patreon.com"); - - // TODO: - // for loader fields? - - // Cleanup test db - test_env.cleanup().await; + assert_eq!(project.slug.unwrap(), "newslug"); + assert_eq!(project.title, "New successful title"); + assert_eq!(project.description, "New successful description"); + assert_eq!(project.body, "New successful body"); + assert_eq!(project.categories, vec![DUMMY_CATEGORIES[0]]); + assert_eq!(project.license.id, "MIT"); + assert_eq!(project.issues_url, Some("https://github.com".to_string())); + assert_eq!(project.discord_url, Some("https://discord.gg".to_string())); + assert_eq!(project.wiki_url, Some("https://wiki.com".to_string())); + assert_eq!(project.donation_urls.unwrap()[0].url, "https://patreon.com"); + + // TODO: + // for loader fields? + }) + .await; } #[actix_rt::test] pub async fn test_bulk_edit_categories() { - with_test_environment(|test_env| async move { - let api = &test_env.v3; + with_test_environment_all(None, |test_env| async move { + let api = &test_env.api; let alpha_project_id: &str = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let beta_project_id: &str = &test_env.dummy.as_ref().unwrap().project_beta.project_id; let resp = api .edit_project_bulk( - [alpha_project_id, beta_project_id], + &[alpha_project_id, beta_project_id], json!({ "categories": [DUMMY_CATEGORIES[0], DUMMY_CATEGORIES[3]], "add_categories": [DUMMY_CATEGORIES[1], DUMMY_CATEGORIES[2]], @@ -461,13 +471,13 @@ pub async fn test_bulk_edit_categories() { assert_eq!(resp.status(), StatusCode::NO_CONTENT); let alpha_body = api - .get_project_deserialized(alpha_project_id, ADMIN_USER_PAT) + .get_project_deserialized_common(alpha_project_id, ADMIN_USER_PAT) .await; assert_eq!(alpha_body.categories, DUMMY_CATEGORIES[0..=2]); assert_eq!(alpha_body.additional_categories, DUMMY_CATEGORIES[4..=5]); let beta_body = api - .get_project_deserialized(beta_project_id, ADMIN_USER_PAT) + .get_project_deserialized_common(beta_project_id, ADMIN_USER_PAT) .await; assert_eq!(beta_body.categories, alpha_body.categories); assert_eq!( @@ -480,527 +490,528 @@ pub async fn test_bulk_edit_categories() { #[actix_rt::test] async fn permissions_patch_project() { - let test_env = TestEnvironment::build(Some(8)).await; - let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; - - // For each permission covered by EDIT_DETAILS, ensure the permission is required - let edit_details = ProjectPermissions::EDIT_DETAILS; - let test_pairs = [ - // Body, status, requested_status tested separately - ("slug", json!("")), // generated in the test to not collide slugs - ("title", json!("randomname")), - ("description", json!("randomdescription")), - ("categories", json!(["combat", "economy"])), - ("additional_categories", json!(["decoration"])), - ("issues_url", json!("https://issues.com")), - ("source_url", json!("https://source.com")), - ("wiki_url", json!("https://wiki.com")), - ( - "donation_urls", - json!([{ - "id": "paypal", - "platform": "Paypal", - "url": "https://paypal.com" - }]), - ), - ("discord_url", json!("https://discord.com")), - ("license_id", json!("MIT")), - ]; - - futures::stream::iter(test_pairs) - .map(|(key, value)| { - let test_env = test_env.clone(); - async move { - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) - .set_json(json!({ - key: if key == "slug" { - json!(generate_random_name("randomslug")) - } else { - value.clone() - }, - })) - }; - PermissionsTest::new(&test_env) - .simple_project_permissions_test(edit_details, req_gen) - .await - .into_iter(); - } - }) - .buffer_unordered(4) - .collect::>() - .await; - - // Test with status and requested_status - // This requires a project with a version, so we use alpha_project_id - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) - .set_json(json!({ - "status": "private", - "requested_status": "private", - })) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(edit_details, req_gen) - .await - .unwrap(); - - // Bulk patch projects - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( - "/v3/projects?ids=[{uri}]", - uri = urlencoding::encode(&format!("\"{}\"", ctx.project_id.unwrap())) - )) - .set_json(json!({ - "name": "randomname", - })) - }; - PermissionsTest::new(&test_env) - .simple_project_permissions_test(edit_details, req_gen) - .await - .unwrap(); - - // Edit body - // Cannot bulk edit body - let edit_body = ProjectPermissions::EDIT_BODY; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) - .set_json(json!({ - "body": "new body!", - })) - }; - PermissionsTest::new(&test_env) - .simple_project_permissions_test(edit_body, req_gen) - .await - .unwrap(); + with_test_environment_all(Some(8), |test_env| async move { + let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + + // For each permission covered by EDIT_DETAILS, ensure the permission is required + let edit_details = ProjectPermissions::EDIT_DETAILS; + let test_pairs = [ + // Body, status, requested_status tested separately + ("slug", json!("")), // generated in the test to not collide slugs + ("title", json!("randomname")), + ("description", json!("randomdescription")), + ("categories", json!(["combat", "economy"])), + ("additional_categories", json!(["decoration"])), + ("issues_url", json!("https://issues.com")), + ("source_url", json!("https://source.com")), + ("wiki_url", json!("https://wiki.com")), + ( + "donation_urls", + json!([{ + "id": "paypal", + "platform": "Paypal", + "url": "https://paypal.com" + }]), + ), + ("discord_url", json!("https://discord.com")), + ("license_id", json!("MIT")), + ]; + + futures::stream::iter(test_pairs) + .map(|(key, value)| { + let test_env = test_env.clone(); + async move { + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) + .set_json(json!({ + key: if key == "slug" { + json!(generate_random_name("randomslug")) + } else { + value.clone() + }, + })) + }; + PermissionsTest::new(&test_env) + .simple_project_permissions_test(edit_details, req_gen) + .await + .into_iter(); + } + }) + .buffer_unordered(4) + .collect::>() + .await; - test_env.cleanup().await; + // Test with status and requested_status + // This requires a project with a version, so we use alpha_project_id + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) + .set_json(json!({ + "status": "private", + "requested_status": "private", + })) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(edit_details, req_gen) + .await + .unwrap(); + + // Bulk patch projects + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!( + "/v3/projects?ids=[{uri}]", + uri = urlencoding::encode(&format!("\"{}\"", ctx.project_id.unwrap())) + )) + .set_json(json!({ + "name": "randomname", + })) + }; + PermissionsTest::new(&test_env) + .simple_project_permissions_test(edit_details, req_gen) + .await + .unwrap(); + + // Edit body + // Cannot bulk edit body + let edit_body = ProjectPermissions::EDIT_BODY; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) + .set_json(json!({ + "body": "new body!", + })) + }; + PermissionsTest::new(&test_env) + .simple_project_permissions_test(edit_body, req_gen) + .await + .unwrap(); + }) + .await; } // Not covered by PATCH /project #[actix_rt::test] async fn permissions_edit_details() { - let test_env = TestEnvironment::build(None).await; - - let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; - let beta_project_id = &test_env.dummy.as_ref().unwrap().project_beta.project_id; - let beta_team_id = &test_env.dummy.as_ref().unwrap().project_beta.team_id; - let beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; - let edit_details = ProjectPermissions::EDIT_DETAILS; - - // Approve beta version as private so we can schedule it - let req = test::TestRequest::patch() - .uri(&format!("/v3/version/{beta_version_id}")) - .append_header(("Authorization", MOD_USER_PAT)) - .set_json(json!({ - "status": "unlisted" - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // Schedule version - let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!("/v3/version/{beta_version_id}/schedule")) // beta_version_id is an *approved* version, so we can schedule it - .set_json(json!( - { - "requested_status": "archived", - "time": Utc::now() + Duration::days(1), - } - )) - }; - PermissionsTest::new(&test_env) - .with_existing_project(beta_project_id, beta_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(edit_details, req_gen) - .await - .unwrap(); - - // Icon edit - // Uses alpha project to delete this icon - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( + with_test_environment_all(None, |test_env| async move { + let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + let beta_project_id = &test_env.dummy.as_ref().unwrap().project_beta.project_id; + let beta_team_id = &test_env.dummy.as_ref().unwrap().project_beta.team_id; + let beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; + let edit_details = ProjectPermissions::EDIT_DETAILS; + + // Approve beta version as private so we can schedule it + let req = test::TestRequest::patch() + .uri(&format!("/v3/version/{beta_version_id}")) + .append_header(("Authorization", MOD_USER_PAT)) + .set_json(json!({ + "status": "unlisted" + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // Schedule version + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::post() + .uri(&format!("/v3/version/{beta_version_id}/schedule")) // beta_version_id is an *approved* version, so we can schedule it + .set_json(json!( + { + "requested_status": "archived", + "time": Utc::now() + Duration::days(1), + } + )) + }; + PermissionsTest::new(&test_env) + .with_existing_project(beta_project_id, beta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(edit_details, req_gen) + .await + .unwrap(); + + // Icon edit + // Uses alpha project to delete this icon + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!( + "/v3/project/{}/icon?ext=png", + ctx.project_id.unwrap() + )) + .set_payload(Bytes::from( + include_bytes!("../tests/files/200x200.png") as &[u8] + )) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(edit_details, req_gen) + .await + .unwrap(); + + // Icon delete + // Uses alpha project to delete added icon + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( "/v3/project/{}/icon?ext=png", ctx.project_id.unwrap() )) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] - )) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(edit_details, req_gen) - .await - .unwrap(); - - // Icon delete - // Uses alpha project to delete added icon - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/project/{}/icon?ext=png", - ctx.project_id.unwrap() - )) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(edit_details, req_gen) - .await - .unwrap(); - - // Add gallery item - // Uses alpha project to add gallery item so we can get its url - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!( - "/v3/project/{}/gallery?ext=png&featured=true", + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(edit_details, req_gen) + .await + .unwrap(); + + // Add gallery item + // Uses alpha project to add gallery item so we can get its url + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::post() + .uri(&format!( + "/v3/project/{}/gallery?ext=png&featured=true", + ctx.project_id.unwrap() + )) + .set_payload(Bytes::from( + include_bytes!("../tests/files/200x200.png") as &[u8] + )) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(edit_details, req_gen) + .await + .unwrap(); + // Get project, as we need the gallery image url + let req = test::TestRequest::get() + .uri(&format!("/v3/project/{alpha_project_id}")) + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + let project: serde_json::Value = test::read_body_json(resp).await; + let gallery_url = project["gallery"][0]["url"].as_str().unwrap(); + + // Edit gallery item + // Uses alpha project to edit gallery item + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch().uri(&format!( + "/v3/project/{}/gallery?url={gallery_url}", ctx.project_id.unwrap() )) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(edit_details, req_gen) + .await + .unwrap(); + + // Remove gallery item + // Uses alpha project to remove gallery item + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( + "/v3/project/{}/gallery?url={gallery_url}", + ctx.project_id.unwrap() )) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(edit_details, req_gen) - .await - .unwrap(); - // Get project, as we need the gallery image url - let req = test::TestRequest::get() - .uri(&format!("/v3/project/{alpha_project_id}")) - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - let project: serde_json::Value = test::read_body_json(resp).await; - let gallery_url = project["gallery"][0]["url"].as_str().unwrap(); - - // Edit gallery item - // Uses alpha project to edit gallery item - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch().uri(&format!( - "/v3/project/{}/gallery?url={gallery_url}", - ctx.project_id.unwrap() - )) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(edit_details, req_gen) - .await - .unwrap(); - - // Remove gallery item - // Uses alpha project to remove gallery item - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/project/{}/gallery?url={gallery_url}", - ctx.project_id.unwrap() - )) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(edit_details, req_gen) - .await - .unwrap(); + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(edit_details, req_gen) + .await + .unwrap(); + }) + .await; } #[actix_rt::test] async fn permissions_upload_version() { - let test_env = TestEnvironment::build(None).await; - let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; - let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; - let alpha_file_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash; - - let upload_version = ProjectPermissions::UPLOAD_VERSION; - - // Upload version with basic-mod.jar - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post().uri("/v3/version").set_multipart([ - MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text( - serde_json::to_string(&json!({ - "project_id": ctx.project_id.unwrap(), - "file_parts": ["basic-mod.jar"], - "version_number": "1.0.0", - "version_title": "1.0.0", - "version_type": "release", - "client_side": "required", - "server_side": "optional", - "dependencies": [], - "game_versions": ["1.20.1"], - "loaders": ["fabric"], - "featured": false, - - })) - .unwrap(), - ), - }, - MultipartSegment { - name: "basic-mod.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../tests/files/basic-mod.jar").to_vec(), - ), - }, - ]) - }; - PermissionsTest::new(&test_env) - .simple_project_permissions_test(upload_version, req_gen) - .await - .unwrap(); - - // Upload file to existing version - // Uses alpha project, as it has an existing version - let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!("/v3/version/{}/file", alpha_version_id)) - .set_multipart([ + with_test_environment_all(None, |test_env| async move { + let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + let alpha_file_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash; + + let upload_version = ProjectPermissions::UPLOAD_VERSION; + + // Upload version with basic-mod.jar + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::post().uri("/v3/version").set_multipart([ MultipartSegment { name: "data".to_string(), filename: None, content_type: Some("application/json".to_string()), data: MultipartSegmentData::Text( serde_json::to_string(&json!({ - "file_parts": ["basic-mod-different.jar"], + "project_id": ctx.project_id.unwrap(), + "file_parts": ["basic-mod.jar"], + "version_number": "1.0.0", + "version_title": "1.0.0", + "version_type": "release", + "client_side": "required", + "server_side": "optional", + "dependencies": [], + "game_versions": ["1.20.1"], + "loaders": ["fabric"], + "featured": false, + })) .unwrap(), ), }, MultipartSegment { - name: "basic-mod-different.jar".to_string(), - filename: Some("basic-mod-different.jar".to_string()), + name: "basic-mod.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), content_type: Some("application/java-archive".to_string()), data: MultipartSegmentData::Binary( - include_bytes!("../tests/files/basic-mod-different.jar").to_vec(), + include_bytes!("../tests/files/basic-mod.jar").to_vec(), ), }, ]) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(upload_version, req_gen) - .await - .unwrap(); - - // Patch version - // Uses alpha project, as it has an existing version - let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v3/version/{}", alpha_version_id)) - .set_json(json!({ - "name": "Basic Mod", - })) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(upload_version, req_gen) - .await - .unwrap(); - - // Delete version file - // Uses alpha project, as it has an existing version - let delete_version = ProjectPermissions::DELETE_VERSION; - let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!("/v3/version_file/{}", alpha_file_hash)) - }; - - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(delete_version, req_gen) - .await - .unwrap(); - - // Delete version - // Uses alpha project, as it has an existing version - let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!("/v3/version/{}", alpha_version_id)) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(delete_version, req_gen) - .await - .unwrap(); - - test_env.cleanup().await; + }; + PermissionsTest::new(&test_env) + .simple_project_permissions_test(upload_version, req_gen) + .await + .unwrap(); + + // Upload file to existing version + // Uses alpha project, as it has an existing version + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::post() + .uri(&format!("/v3/version/{}/file", alpha_version_id)) + .set_multipart([ + MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text( + serde_json::to_string(&json!({ + "file_parts": ["basic-mod-different.jar"], + })) + .unwrap(), + ), + }, + MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod-different.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../tests/files/basic-mod-different.jar").to_vec(), + ), + }, + ]) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(upload_version, req_gen) + .await + .unwrap(); + + // Patch version + // Uses alpha project, as it has an existing version + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!("/v3/version/{}", alpha_version_id)) + .set_json(json!({ + "name": "Basic Mod", + })) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(upload_version, req_gen) + .await + .unwrap(); + + // Delete version file + // Uses alpha project, as it has an existing version + let delete_version = ProjectPermissions::DELETE_VERSION; + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!("/v3/version_file/{}", alpha_file_hash)) + }; + + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(delete_version, req_gen) + .await + .unwrap(); + + // Delete version + // Uses alpha project, as it has an existing version + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!("/v3/version/{}", alpha_version_id)) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(delete_version, req_gen) + .await + .unwrap(); + }) + .await; } #[actix_rt::test] async fn permissions_manage_invites() { // Add member, remove member, edit member - let test_env = TestEnvironment::build(None).await; - let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + with_test_environment_all(None, |test_env| async move { + let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + + let manage_invites = ProjectPermissions::MANAGE_INVITES; + + // Add member + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::post() + .uri(&format!("/v3/team/{}/members", ctx.team_id.unwrap())) + .set_json(json!({ + "user_id": MOD_USER_ID, + "permissions": 0, + })) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(manage_invites, req_gen) + .await + .unwrap(); + + // Edit member + let edit_member = ProjectPermissions::EDIT_MEMBER; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!( + "/v3/team/{}/members/{MOD_USER_ID}", + ctx.team_id.unwrap() + )) + .set_json(json!({ + "permissions": 0, + })) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(edit_member, req_gen) + .await + .unwrap(); - let manage_invites = ProjectPermissions::MANAGE_INVITES; + // remove member + // requires manage_invites if they have not yet accepted the invite + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( + "/v3/team/{}/members/{MOD_USER_ID}", + ctx.team_id.unwrap() + )) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(manage_invites, req_gen) + .await + .unwrap(); - // Add member - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!("/v3/team/{}/members", ctx.team_id.unwrap())) + // re-add member for testing + let req = test::TestRequest::post() + .uri(&format!("/v3/team/{}/members", alpha_team_id)) + .append_header(("Authorization", ADMIN_USER_PAT)) .set_json(json!({ "user_id": MOD_USER_ID, - "permissions": 0, })) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(manage_invites, req_gen) - .await - .unwrap(); - - // Edit member - let edit_member = ProjectPermissions::EDIT_MEMBER; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!( + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // Accept invite + let req = test::TestRequest::post() + .uri(&format!("/v3/team/{}/join", alpha_team_id)) + .append_header(("Authorization", MOD_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // remove existing member (requires remove_member) + let remove_member = ProjectPermissions::REMOVE_MEMBER; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( "/v3/team/{}/members/{MOD_USER_ID}", ctx.team_id.unwrap() )) - .set_json(json!({ - "permissions": 0, - })) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(edit_member, req_gen) - .await - .unwrap(); - - // remove member - // requires manage_invites if they have not yet accepted the invite - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/team/{}/members/{MOD_USER_ID}", - ctx.team_id.unwrap() - )) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(manage_invites, req_gen) - .await - .unwrap(); - - // re-add member for testing - let req = test::TestRequest::post() - .uri(&format!("/v3/team/{}/members", alpha_team_id)) - .append_header(("Authorization", ADMIN_USER_PAT)) - .set_json(json!({ - "user_id": MOD_USER_ID, - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // Accept invite - let req = test::TestRequest::post() - .uri(&format!("/v3/team/{}/join", alpha_team_id)) - .append_header(("Authorization", MOD_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // remove existing member (requires remove_member) - let remove_member = ProjectPermissions::REMOVE_MEMBER; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!( - "/v3/team/{}/members/{MOD_USER_ID}", - ctx.team_id.unwrap() - )) - }; - - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(remove_member, req_gen) - .await - .unwrap(); - - test_env.cleanup().await; + }; + + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(remove_member, req_gen) + .await + .unwrap(); + }) + .await; } #[actix_rt::test] async fn permissions_delete_project() { // Add member, remove member, edit member - let test_env = TestEnvironment::build(None).await; - - let delete_project = ProjectPermissions::DELETE_PROJECT; - - // Delete project - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) - }; - PermissionsTest::new(&test_env) - .simple_project_permissions_test(delete_project, req_gen) - .await - .unwrap(); + with_test_environment_all(None, |test_env| async move { + let delete_project = ProjectPermissions::DELETE_PROJECT; + + // Delete project + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) + }; + PermissionsTest::new(&test_env) + .simple_project_permissions_test(delete_project, req_gen) + .await + .unwrap(); - test_env.cleanup().await; + test_env.cleanup().await; + }) + .await; } #[actix_rt::test] async fn project_permissions_consistency_test() { - let test_env = TestEnvironment::build(Some(10)).await; - - // Test that the permissions are consistent with each other - // For example, if we get the projectpermissions directly, from an organization's defaults, overriden, etc, they should all be correct & consistent - - // Full project permissions test with EDIT_DETAILS - let success_permissions = ProjectPermissions::EDIT_DETAILS; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) - .set_json(json!({ - "title": "Example title - changed.", - })) - }; - PermissionsTest::new(&test_env) - .full_project_permissions_test(success_permissions, req_gen) - .await - .unwrap(); - - // We do a test with more specific permissions, to ensure that *exactly* the permissions at each step are as expected - let success_permissions = ProjectPermissions::EDIT_DETAILS - | ProjectPermissions::REMOVE_MEMBER - | ProjectPermissions::DELETE_VERSION - | ProjectPermissions::VIEW_PAYOUTS; - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) - .set_json(json!({ - "title": "Example title - changed.", - })) - }; - PermissionsTest::new(&test_env) - .full_project_permissions_test(success_permissions, req_gen) - .await - .unwrap(); - - test_env.cleanup().await; + with_test_environment_all(Some(10), |test_env| async move { + // Test that the permissions are consistent with each other + // For example, if we get the projectpermissions directly, from an organization's defaults, overriden, etc, they should all be correct & consistent + + // Full project permissions test with EDIT_DETAILS + let success_permissions = ProjectPermissions::EDIT_DETAILS; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) + .set_json(json!({ + "title": "Example title - changed.", + })) + }; + PermissionsTest::new(&test_env) + .full_project_permissions_test(success_permissions, req_gen) + .await + .unwrap(); + + // We do a test with more specific permissions, to ensure that *exactly* the permissions at each step are as expected + let success_permissions = ProjectPermissions::EDIT_DETAILS + | ProjectPermissions::REMOVE_MEMBER + | ProjectPermissions::DELETE_VERSION + | ProjectPermissions::VIEW_PAYOUTS; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) + .set_json(json!({ + "title": "Example title - changed.", + })) + }; + PermissionsTest::new(&test_env) + .full_project_permissions_test(success_permissions, req_gen) + .await + .unwrap(); + }) + .await; } // Route tests: diff --git a/tests/scopes.rs b/tests/scopes.rs index e70f037f..4b692058 100644 --- a/tests/scopes.rs +++ b/tests/scopes.rs @@ -1,11 +1,15 @@ use actix_web::test::{self, TestRequest}; use bytes::Bytes; use chrono::{Duration, Utc}; -use common::{database::*, environment::TestEnvironment, scopes::ScopeTest}; + +use common::environment::with_test_environment_all; +use common::{database::*, scopes::ScopeTest}; use labrinth::models::pats::Scopes; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; use serde_json::json; +use crate::common::api_common::ApiTeams; + // For each scope, we (using test_scope): // - create a PAT with a given set of scopes for a function // - create a PAT with all other scopes for a function @@ -18,1335 +22,1333 @@ mod common; #[actix_rt::test] async fn user_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - - // User reading - let read_user = Scopes::USER_READ; - let req_gen = || TestRequest::get().uri("/v3/user"); - let (_, success) = ScopeTest::new(&test_env) - .test(req_gen, read_user) - .await - .unwrap(); - assert!(success["email"].as_str().is_none()); // email should not be present - assert!(success["payout_data"].as_object().is_none()); // payout should not be present - - // Email reading - let read_email = Scopes::USER_READ | Scopes::USER_READ_EMAIL; - let req_gen = || TestRequest::get().uri("/v3/user"); - let (_, success) = ScopeTest::new(&test_env) - .test(req_gen, read_email) - .await - .unwrap(); - assert_eq!(success["email"], json!("user@modrinth.com")); // email should be present - - // Payout reading - let read_payout = Scopes::USER_READ | Scopes::PAYOUTS_READ; - let req_gen = || TestRequest::get().uri("/v3/user"); - let (_, success) = ScopeTest::new(&test_env) - .test(req_gen, read_payout) - .await - .unwrap(); - assert!(success["payout_data"].as_object().is_some()); // payout should be present - - // User writing - // We use the Admin PAT for this test, on the 'user' user - let write_user = Scopes::USER_WRITE; - let req_gen = || { - TestRequest::patch().uri("/v3/user/user").set_json(json!( { - // Do not include 'username', as to not change the rest of the tests - "name": "NewName", - "bio": "New bio", - "location": "New location", - "role": "admin", - "badges": 5, - // Do not include payout info, different scope - })) - }; - ScopeTest::new(&test_env) - .with_user_id(ADMIN_USER_ID_PARSED) - .test(req_gen, write_user) - .await - .unwrap(); - - // User deletion - // (The failure is first, and this is the last test for this test function, we can delete it and use the same PAT for both tests) - let delete_user = Scopes::USER_DELETE; - let req_gen = || TestRequest::delete().uri("/v3/user/enemy"); - ScopeTest::new(&test_env) - .with_user_id(ENEMY_USER_ID_PARSED) - .test(req_gen, delete_user) - .await - .unwrap(); - - // Cleanup test db - test_env.cleanup().await; + with_test_environment_all(None, |test_env| async move { + // User reading + let read_user = Scopes::USER_READ; + let req_gen = || TestRequest::get().uri("/v3/user"); + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, read_user) + .await + .unwrap(); + assert!(success["email"].as_str().is_none()); // email should not be present + assert!(success["payout_data"].as_object().is_none()); // payout should not be present + + // Email reading + let read_email = Scopes::USER_READ | Scopes::USER_READ_EMAIL; + let req_gen = || TestRequest::get().uri("/v3/user"); + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, read_email) + .await + .unwrap(); + assert_eq!(success["email"], json!("user@modrinth.com")); // email should be present + + // Payout reading + let read_payout = Scopes::USER_READ | Scopes::PAYOUTS_READ; + let req_gen = || TestRequest::get().uri("/v3/user"); + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, read_payout) + .await + .unwrap(); + assert!(success["payout_data"].as_object().is_some()); // payout should be present + + // User writing + // We use the Admin PAT for this test, on the 'user' user + let write_user = Scopes::USER_WRITE; + let req_gen = || { + TestRequest::patch().uri("/v3/user/user").set_json(json!( { + // Do not include 'username', as to not change the rest of the tests + "name": "NewName", + "bio": "New bio", + "location": "New location", + "role": "admin", + "badges": 5, + // Do not include payout info, different scope + })) + }; + ScopeTest::new(&test_env) + .with_user_id(ADMIN_USER_ID_PARSED) + .test(req_gen, write_user) + .await + .unwrap(); + + // User deletion + // (The failure is first, and this is the last test for this test function, we can delete it and use the same PAT for both tests) + let delete_user = Scopes::USER_DELETE; + let req_gen = || TestRequest::delete().uri("/v3/user/enemy"); + ScopeTest::new(&test_env) + .with_user_id(ENEMY_USER_ID_PARSED) + .test(req_gen, delete_user) + .await + .unwrap(); + }) + .await; } // Notifications #[actix_rt::test] pub async fn notifications_scopes() { - let test_env = TestEnvironment::build(None).await; - let alpha_team_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_alpha - .team_id - .clone(); - - // We will invite user 'friend' to project team, and use that as a notification - // Get notifications - let resp = test_env - .v3 - .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) - .await; - assert_eq!(resp.status(), 204); - - // Notification get - let read_notifications = Scopes::NOTIFICATION_READ; - let req_gen = - || test::TestRequest::get().uri(&format!("/v3/user/{FRIEND_USER_ID}/notifications")); - let (_, success) = ScopeTest::new(&test_env) - .with_user_id(FRIEND_USER_ID_PARSED) - .test(req_gen, read_notifications) - .await - .unwrap(); - let notification_id = success[0]["id"].as_str().unwrap(); - - let req_gen = || { - test::TestRequest::get().uri(&format!( - "/v3/notifications?ids=[{uri}]", - uri = urlencoding::encode(&format!("\"{notification_id}\"")) - )) - }; - ScopeTest::new(&test_env) - .with_user_id(FRIEND_USER_ID_PARSED) - .test(req_gen, read_notifications) - .await - .unwrap(); - - let req_gen = || test::TestRequest::get().uri(&format!("/v3/notification/{notification_id}")); - ScopeTest::new(&test_env) - .with_user_id(FRIEND_USER_ID_PARSED) - .test(req_gen, read_notifications) - .await - .unwrap(); - - // Notification mark as read - let write_notifications = Scopes::NOTIFICATION_WRITE; - let req_gen = || { - test::TestRequest::patch().uri(&format!( - "/v3/notifications?ids=[{uri}]", - uri = urlencoding::encode(&format!("\"{notification_id}\"")) - )) - }; - ScopeTest::new(&test_env) - .with_user_id(FRIEND_USER_ID_PARSED) - .test(req_gen, write_notifications) - .await - .unwrap(); - - let req_gen = || test::TestRequest::patch().uri(&format!("/v3/notification/{notification_id}")); - ScopeTest::new(&test_env) - .with_user_id(FRIEND_USER_ID_PARSED) - .test(req_gen, write_notifications) - .await - .unwrap(); - - // Notification delete - let req_gen = - || test::TestRequest::delete().uri(&format!("/v3/notification/{notification_id}")); - ScopeTest::new(&test_env) - .with_user_id(FRIEND_USER_ID_PARSED) - .test(req_gen, write_notifications) - .await - .unwrap(); - - // Mass notification delete - // We invite mod, get the notification ID, and do mass delete using that - let resp = test_env - .v3 - .add_user_to_team(alpha_team_id, MOD_USER_ID, None, None, USER_USER_PAT) - .await; - assert_eq!(resp.status(), 204); - let read_notifications = Scopes::NOTIFICATION_READ; - let req_gen = || test::TestRequest::get().uri(&format!("/v3/user/{MOD_USER_ID}/notifications")); - let (_, success) = ScopeTest::new(&test_env) - .with_user_id(MOD_USER_ID_PARSED) - .test(req_gen, read_notifications) - .await - .unwrap(); - let notification_id = success[0]["id"].as_str().unwrap(); - - let req_gen = || { - test::TestRequest::delete().uri(&format!( - "/v3/notifications?ids=[{uri}]", - uri = urlencoding::encode(&format!("\"{notification_id}\"")) - )) - }; - ScopeTest::new(&test_env) - .with_user_id(MOD_USER_ID_PARSED) - .test(req_gen, write_notifications) - .await - .unwrap(); - - // Cleanup test db - test_env.cleanup().await; + with_test_environment_all(None, |test_env| async move { + let alpha_team_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .team_id + .clone(); + + // We will invite user 'friend' to project team, and use that as a notification + // Get notifications + let resp = test_env + .api + .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 204); + + // Notification get + let read_notifications = Scopes::NOTIFICATION_READ; + let req_gen = + || test::TestRequest::get().uri(&format!("/v3/user/{FRIEND_USER_ID}/notifications")); + let (_, success) = ScopeTest::new(&test_env) + .with_user_id(FRIEND_USER_ID_PARSED) + .test(req_gen, read_notifications) + .await + .unwrap(); + let notification_id = success[0]["id"].as_str().unwrap(); + + let req_gen = || { + test::TestRequest::get().uri(&format!( + "/v3/notifications?ids=[{uri}]", + uri = urlencoding::encode(&format!("\"{notification_id}\"")) + )) + }; + ScopeTest::new(&test_env) + .with_user_id(FRIEND_USER_ID_PARSED) + .test(req_gen, read_notifications) + .await + .unwrap(); + + let req_gen = + || test::TestRequest::get().uri(&format!("/v3/notification/{notification_id}")); + ScopeTest::new(&test_env) + .with_user_id(FRIEND_USER_ID_PARSED) + .test(req_gen, read_notifications) + .await + .unwrap(); + + // Notification mark as read + let write_notifications = Scopes::NOTIFICATION_WRITE; + let req_gen = || { + test::TestRequest::patch().uri(&format!( + "/v3/notifications?ids=[{uri}]", + uri = urlencoding::encode(&format!("\"{notification_id}\"")) + )) + }; + ScopeTest::new(&test_env) + .with_user_id(FRIEND_USER_ID_PARSED) + .test(req_gen, write_notifications) + .await + .unwrap(); + + let req_gen = + || test::TestRequest::patch().uri(&format!("/v3/notification/{notification_id}")); + ScopeTest::new(&test_env) + .with_user_id(FRIEND_USER_ID_PARSED) + .test(req_gen, write_notifications) + .await + .unwrap(); + + // Notification delete + let req_gen = + || test::TestRequest::delete().uri(&format!("/v3/notification/{notification_id}")); + ScopeTest::new(&test_env) + .with_user_id(FRIEND_USER_ID_PARSED) + .test(req_gen, write_notifications) + .await + .unwrap(); + + // Mass notification delete + // We invite mod, get the notification ID, and do mass delete using that + let resp = test_env + .api + .add_user_to_team(alpha_team_id, MOD_USER_ID, None, None, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 204); + let read_notifications = Scopes::NOTIFICATION_READ; + let req_gen = + || test::TestRequest::get().uri(&format!("/v3/user/{MOD_USER_ID}/notifications")); + let (_, success) = ScopeTest::new(&test_env) + .with_user_id(MOD_USER_ID_PARSED) + .test(req_gen, read_notifications) + .await + .unwrap(); + let notification_id = success[0]["id"].as_str().unwrap(); + + let req_gen = || { + test::TestRequest::delete().uri(&format!( + "/v3/notifications?ids=[{uri}]", + uri = urlencoding::encode(&format!("\"{notification_id}\"")) + )) + }; + ScopeTest::new(&test_env) + .with_user_id(MOD_USER_ID_PARSED) + .test(req_gen, write_notifications) + .await + .unwrap(); + }) + .await; } // Project version creation scopes #[actix_rt::test] pub async fn project_version_create_scopes() { - let test_env = TestEnvironment::build(None).await; - - // Create project - let create_project = Scopes::PROJECT_CREATE; - let json_data = json!( - { - "title": "Test_Add_Project project", - "slug": "demo", - "description": "Example description.", - "body": "Example body.", - "initial_versions": [{ - "file_parts": ["basic-mod.jar"], - "version_number": "1.2.3", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "client_side": "required", - "server_side": "optional", - "release_channel": "release", - "loaders": ["fabric"], - "featured": true - }], - "categories": [], - "license_id": "MIT" - } - ); - let json_segment = MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - }; - let file_segment = MultipartSegment { - name: "basic-mod.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary(include_bytes!("../tests/files/basic-mod.jar").to_vec()), - }; - - let req_gen = || { - test::TestRequest::post() - .uri("/v3/project") - .set_multipart(vec![json_segment.clone(), file_segment.clone()]) - }; - let (_, success) = ScopeTest::new(&test_env) - .test(req_gen, create_project) - .await - .unwrap(); - let project_id = success["id"].as_str().unwrap(); - - // Add version to project - let create_version = Scopes::VERSION_CREATE; - let json_data = json!( + with_test_environment_all(None, |test_env| async move { + // Create project + let create_project = Scopes::PROJECT_CREATE; + let json_data = json!( { - "project_id": project_id, - "file_parts": ["basic-mod-different.jar"], - "version_number": "1.2.3.4", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "client_side": "required", - "server_side": "optional", - "release_channel": "release", - "loaders": ["fabric"], - "featured": true + "title": "Test_Add_Project project", + "slug": "demo", + "description": "Example description.", + "body": "Example body.", + "initial_versions": [{ + "file_parts": ["basic-mod.jar"], + "version_number": "1.2.3", + "version_title": "start", + "dependencies": [], + "game_versions": ["1.20.1"] , + "client_side": "required", + "server_side": "optional", + "release_channel": "release", + "loaders": ["fabric"], + "featured": true + }], + "categories": [], + "license_id": "MIT" } - ); - let json_segment = MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - }; - let file_segment = MultipartSegment { - name: "basic-mod-different.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../tests/files/basic-mod-different.jar").to_vec(), - ), - }; - - let req_gen = || { - test::TestRequest::post() - .uri("/v3/version") - .set_multipart(vec![json_segment.clone(), file_segment.clone()]) - }; - ScopeTest::new(&test_env) - .test(req_gen, create_version) - .await - .unwrap(); - - // Cleanup test db - test_env.cleanup().await; + ); + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + }; + let file_segment = MultipartSegment { + name: "basic-mod.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../tests/files/basic-mod.jar").to_vec(), + ), + }; + + let req_gen = || { + test::TestRequest::post() + .uri("/v3/project") + .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + }; + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, create_project) + .await + .unwrap(); + let project_id = success["id"].as_str().unwrap(); + + // Add version to project + let create_version = Scopes::VERSION_CREATE; + let json_data = json!( + { + "project_id": project_id, + "file_parts": ["basic-mod-different.jar"], + "version_number": "1.2.3.4", + "version_title": "start", + "dependencies": [], + "game_versions": ["1.20.1"] , + "client_side": "required", + "server_side": "optional", + "release_channel": "release", + "loaders": ["fabric"], + "featured": true + } + ); + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + }; + let file_segment = MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../tests/files/basic-mod-different.jar").to_vec(), + ), + }; + + let req_gen = || { + test::TestRequest::post() + .uri("/v3/version") + .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + }; + ScopeTest::new(&test_env) + .test(req_gen, create_version) + .await + .unwrap(); + }) + .await; } // Project management scopes #[actix_rt::test] pub async fn project_version_reads_scopes() { - let test_env = TestEnvironment::build(None).await; - let beta_project_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_beta - .project_id - .clone(); - let beta_version_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_beta - .version_id - .clone(); - let alpha_team_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_alpha - .team_id - .clone(); - let beta_file_hash = &test_env - .dummy - .as_ref() - .unwrap() - .project_beta - .file_hash - .clone(); - - // Project reading - // Uses 404 as the expected failure code (or 200 and an empty list for mass reads) - let read_project = Scopes::PROJECT_READ; - let req_gen = || test::TestRequest::get().uri(&format!("/v3/project/{beta_project_id}")); - ScopeTest::new(&test_env) - .with_failure_code(404) - .test(req_gen, read_project) - .await - .unwrap(); - - let req_gen = - || test::TestRequest::get().uri(&format!("/v3/project/{beta_project_id}/dependencies")); - ScopeTest::new(&test_env) - .with_failure_code(404) - .test(req_gen, read_project) - .await - .unwrap(); - - let req_gen = || { - test::TestRequest::get().uri(&format!( - "/v3/projects?ids=[{uri}]", - uri = urlencoding::encode(&format!("\"{beta_project_id}\"")) - )) - }; - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .test(req_gen, read_project) - .await - .unwrap(); - assert!(failure.as_array().unwrap().is_empty()); - assert!(!success.as_array().unwrap().is_empty()); - - // Team project reading - let req_gen = - || test::TestRequest::get().uri(&format!("/v3/project/{beta_project_id}/members")); - ScopeTest::new(&test_env) - .with_failure_code(404) - .test(req_gen, read_project) - .await - .unwrap(); - - // Get team members - // In this case, as these are public endpoints, logging in only is relevant to showing permissions - // So for our test project (with 1 user, 'user') we will check the permissions before and after having the scope. - let req_gen = || test::TestRequest::get().uri(&format!("/v3/team/{alpha_team_id}/members")); - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .test(req_gen, read_project) - .await - .unwrap(); - assert!(!failure[0]["permissions"].is_number()); - assert!(success[0]["permissions"].is_number()); - - let req_gen = || { - test::TestRequest::get().uri(&format!( - "/v3/teams?ids=[{uri}]", - uri = urlencoding::encode(&format!("\"{alpha_team_id}\"")) - )) - }; - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .test(req_gen, read_project) - .await - .unwrap(); - assert!(!failure[0][0]["permissions"].is_number()); - assert!(success[0][0]["permissions"].is_number()); - - // User project reading - // Test user has two projects, one public and one private - let req_gen = || test::TestRequest::get().uri(&format!("/v3/user/{USER_USER_ID}/projects")); - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .test(req_gen, read_project) - .await - .unwrap(); - assert!(!failure - .as_array() - .unwrap() - .iter() - .any(|x| x["status"] == "processing")); - assert!(success - .as_array() - .unwrap() - .iter() - .any(|x| x["status"] == "processing")); - - // Project metadata reading - let req_gen = || { - test::TestRequest::get().uri(&format!( - "/maven/maven/modrinth/{beta_project_id}/maven-metadata.xml" - )) - }; - ScopeTest::new(&test_env) - .with_failure_code(404) - .test(req_gen, read_project) - .await - .unwrap(); - - // Version reading - // First, set version to hidden (which is when the scope is required to read it) - let read_version = Scopes::VERSION_READ; - let req = test::TestRequest::patch() - .uri(&format!("/v3/version/{beta_version_id}")) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "status": "draft" - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - let req_gen = || test::TestRequest::get().uri(&format!("/v3/version_file/{beta_file_hash}")); - ScopeTest::new(&test_env) - .with_failure_code(404) - .test(req_gen, read_version) - .await - .unwrap(); - - let req_gen = - || test::TestRequest::get().uri(&format!("/v3/version_file/{beta_file_hash}/download")); - ScopeTest::new(&test_env) - .with_failure_code(404) - .test(req_gen, read_version) - .await - .unwrap(); - - // TODO: Should this be /POST? Looks like /GET - // TODO: this scope doesn't actually affect anything, because the Project::get_id contained within disallows hidden versions, which is the point of this scope - // let req_gen = || { - // test::TestRequest::post() - // .uri(&format!("/v3/version_file/{beta_file_hash}/update")) - // .set_json(json!({})) - // }; - // ScopeTest::new(&test_env).with_failure_code(404).test(req_gen, read_version).await.unwrap(); - - // TODO: Should this be /POST? Looks like /GET - let req_gen = || { - test::TestRequest::post() - .uri("/v3/version_files") + with_test_environment_all(None, |test_env| async move { + let beta_project_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .project_id + .clone(); + let beta_version_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .version_id + .clone(); + let alpha_team_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .team_id + .clone(); + let beta_file_hash = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .file_hash + .clone(); + + // Project reading + // Uses 404 as the expected failure code (or 200 and an empty list for mass reads) + let read_project = Scopes::PROJECT_READ; + let req_gen = || test::TestRequest::get().uri(&format!("/v3/project/{beta_project_id}")); + ScopeTest::new(&test_env) + .with_failure_code(404) + .test(req_gen, read_project) + .await + .unwrap(); + + let req_gen = + || test::TestRequest::get().uri(&format!("/v3/project/{beta_project_id}/dependencies")); + ScopeTest::new(&test_env) + .with_failure_code(404) + .test(req_gen, read_project) + .await + .unwrap(); + + let req_gen = || { + test::TestRequest::get().uri(&format!( + "/v3/projects?ids=[{uri}]", + uri = urlencoding::encode(&format!("\"{beta_project_id}\"")) + )) + }; + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .test(req_gen, read_project) + .await + .unwrap(); + assert!(failure.as_array().unwrap().is_empty()); + assert!(!success.as_array().unwrap().is_empty()); + + // Team project reading + let req_gen = + || test::TestRequest::get().uri(&format!("/v3/project/{beta_project_id}/members")); + ScopeTest::new(&test_env) + .with_failure_code(404) + .test(req_gen, read_project) + .await + .unwrap(); + + // Get team members + // In this case, as these are public endpoints, logging in only is relevant to showing permissions + // So for our test project (with 1 user, 'user') we will check the permissions before and after having the scope. + let req_gen = || test::TestRequest::get().uri(&format!("/v3/team/{alpha_team_id}/members")); + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .test(req_gen, read_project) + .await + .unwrap(); + assert!(!failure[0]["permissions"].is_number()); + assert!(success[0]["permissions"].is_number()); + + let req_gen = || { + test::TestRequest::get().uri(&format!( + "/v3/teams?ids=[{uri}]", + uri = urlencoding::encode(&format!("\"{alpha_team_id}\"")) + )) + }; + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .test(req_gen, read_project) + .await + .unwrap(); + assert!(!failure[0][0]["permissions"].is_number()); + assert!(success[0][0]["permissions"].is_number()); + + // User project reading + // Test user has two projects, one public and one private + let req_gen = || test::TestRequest::get().uri(&format!("/v3/user/{USER_USER_ID}/projects")); + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .test(req_gen, read_project) + .await + .unwrap(); + assert!(!failure + .as_array() + .unwrap() + .iter() + .any(|x| x["status"] == "processing")); + assert!(success + .as_array() + .unwrap() + .iter() + .any(|x| x["status"] == "processing")); + + // Project metadata reading + let req_gen = || { + test::TestRequest::get().uri(&format!( + "/maven/maven/modrinth/{beta_project_id}/maven-metadata.xml" + )) + }; + ScopeTest::new(&test_env) + .with_failure_code(404) + .test(req_gen, read_project) + .await + .unwrap(); + + // Version reading + // First, set version to hidden (which is when the scope is required to read it) + let read_version = Scopes::VERSION_READ; + let req = test::TestRequest::patch() + .uri(&format!("/v3/version/{beta_version_id}")) + .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ - "hashes": [beta_file_hash] + "status": "draft" })) - }; - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .test(req_gen, read_version) - .await - .unwrap(); - assert!(!failure.as_object().unwrap().contains_key(beta_file_hash)); - assert!(success.as_object().unwrap().contains_key(beta_file_hash)); - - // Update version file - // TODO: Should this be /POST? Looks like /GET - // TODO: this scope doesn't actually affect anything, because the Project::get_id contained within disallows hidden versions, which is the point of this scope - - // let req_gen = || { - // test::TestRequest::post() - // .uri(&format!("/v3/version_files/update_individual")) - // .set_json(json!({ - // "hashes": [{ - // "hash": beta_file_hash, - // }] - // })) - // }; - // let (failure, success) = ScopeTest::new(&test_env).with_failure_code(200).test(req_gen, read_version).await.unwrap(); - // assert!(!failure.as_object().unwrap().contains_key(beta_file_hash)); - // assert!(success.as_object().unwrap().contains_key(beta_file_hash)); - - // Update version file - // TODO: this scope doesn't actually affect anything, because the Project::get_id contained within disallows hidden versions, which is the point of this scope - // let req_gen = || { - // test::TestRequest::post() - // .uri(&format!("/v3/version_files/update")) - // .set_json(json!({ - // "hashes": [beta_file_hash] - // })) - // }; - // let (failure, success) = ScopeTest::new(&test_env).with_failure_code(200).test(req_gen, read_version).await.unwrap(); - // assert!(!failure.as_object().unwrap().contains_key(beta_file_hash)); - // assert!(success.as_object().unwrap().contains_key(beta_file_hash)); - - // Both project and version reading - let read_project_and_version = Scopes::PROJECT_READ | Scopes::VERSION_READ; - let req_gen = - || test::TestRequest::get().uri(&format!("/v3/project/{beta_project_id}/version")); - ScopeTest::new(&test_env) - .with_failure_code(404) - .test(req_gen, read_project_and_version) - .await - .unwrap(); - - // TODO: fails for the same reason as above - // let req_gen = || { - // test::TestRequest::get() - // .uri(&format!("/v3/project/{beta_project_id}/version/{beta_version_id}")) - // }; - // ScopeTest::new(&test_env).with_failure_code(404).test(req_gen, read_project_and_version).await.unwrap(); - - // Cleanup test db - test_env.cleanup().await; + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + let req_gen = + || test::TestRequest::get().uri(&format!("/v3/version_file/{beta_file_hash}")); + ScopeTest::new(&test_env) + .with_failure_code(404) + .test(req_gen, read_version) + .await + .unwrap(); + + let req_gen = + || test::TestRequest::get().uri(&format!("/v3/version_file/{beta_file_hash}/download")); + ScopeTest::new(&test_env) + .with_failure_code(404) + .test(req_gen, read_version) + .await + .unwrap(); + + // TODO: Should this be /POST? Looks like /GET + // TODO: this scope doesn't actually affect anything, because the Project::get_id contained within disallows hidden versions, which is the point of this scope + // let req_gen = || { + // test::TestRequest::post() + // .uri(&format!("/v3/version_file/{beta_file_hash}/update")) + // .set_json(json!({})) + // }; + // ScopeTest::new(&test_env).with_failure_code(404).test(req_gen, read_version).await.unwrap(); + + // TODO: Should this be /POST? Looks like /GET + let req_gen = || { + test::TestRequest::post() + .uri("/v3/version_files") + .set_json(json!({ + "hashes": [beta_file_hash] + })) + }; + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .test(req_gen, read_version) + .await + .unwrap(); + assert!(!failure.as_object().unwrap().contains_key(beta_file_hash)); + assert!(success.as_object().unwrap().contains_key(beta_file_hash)); + + // Update version file + // TODO: Should this be /POST? Looks like /GET + // TODO: this scope doesn't actually affect anything, because the Project::get_id contained within disallows hidden versions, which is the point of this scope + + // let req_gen = || { + // test::TestRequest::post() + // .uri(&format!("/v3/version_files/update_individual")) + // .set_json(json!({ + // "hashes": [{ + // "hash": beta_file_hash, + // }] + // })) + // }; + // let (failure, success) = ScopeTest::new(&test_env).with_failure_code(200).test(req_gen, read_version).await.unwrap(); + // assert!(!failure.as_object().unwrap().contains_key(beta_file_hash)); + // assert!(success.as_object().unwrap().contains_key(beta_file_hash)); + + // Update version file + // TODO: this scope doesn't actually affect anything, because the Project::get_id contained within disallows hidden versions, which is the point of this scope + // let req_gen = || { + // test::TestRequest::post() + // .uri(&format!("/v3/version_files/update")) + // .set_json(json!({ + // "hashes": [beta_file_hash] + // })) + // }; + // let (failure, success) = ScopeTest::new(&test_env).with_failure_code(200).test(req_gen, read_version).await.unwrap(); + // assert!(!failure.as_object().unwrap().contains_key(beta_file_hash)); + // assert!(success.as_object().unwrap().contains_key(beta_file_hash)); + + // Both project and version reading + let read_project_and_version = Scopes::PROJECT_READ | Scopes::VERSION_READ; + let req_gen = + || test::TestRequest::get().uri(&format!("/v3/project/{beta_project_id}/version")); + ScopeTest::new(&test_env) + .with_failure_code(404) + .test(req_gen, read_project_and_version) + .await + .unwrap(); + + // TODO: fails for the same reason as above + // let req_gen = || { + // test::TestRequest::get() + // .uri(&format!("/v3/project/{beta_project_id}/version/{beta_version_id}")) + // }; + // ScopeTest::new(&test_env).with_failure_code(404).test(req_gen, read_project_and_version).await.unwrap(); + }) + .await; } // Project writing #[actix_rt::test] pub async fn project_write_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let beta_project_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_beta - .project_id - .clone(); - let alpha_team_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_alpha - .team_id - .clone(); - - // Projects writing - let write_project = Scopes::PROJECT_WRITE; - let req_gen = || { - test::TestRequest::patch() + with_test_environment_all(None, |test_env| async move { + let beta_project_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .project_id + .clone(); + let alpha_team_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .team_id + .clone(); + + // Projects writing + let write_project = Scopes::PROJECT_WRITE; + let req_gen = || { + test::TestRequest::patch() + .uri(&format!("/v3/project/{beta_project_id}")) + .set_json(json!( + { + "title": "test_project_version_write_scopes Title", + } + )) + }; + ScopeTest::new(&test_env) + .test(req_gen, write_project) + .await + .unwrap(); + + let req_gen = || { + test::TestRequest::patch() + .uri(&format!( + "/v3/projects?ids=[{uri}]", + uri = urlencoding::encode(&format!("\"{beta_project_id}\"")) + )) + .set_json(json!( + { + "description": "test_project_version_write_scopes Description", + } + )) + }; + ScopeTest::new(&test_env) + .test(req_gen, write_project) + .await + .unwrap(); + + // Approve beta as private so we can schedule it + let req = test::TestRequest::patch() .uri(&format!("/v3/project/{beta_project_id}")) - .set_json(json!( - { - "title": "test_project_version_write_scopes Title", - } - )) - }; - ScopeTest::new(&test_env) - .test(req_gen, write_project) - .await - .unwrap(); - - let req_gen = || { - test::TestRequest::patch() - .uri(&format!( - "/v3/projects?ids=[{uri}]", - uri = urlencoding::encode(&format!("\"{beta_project_id}\"")) - )) - .set_json(json!( - { - "description": "test_project_version_write_scopes Description", - } - )) - }; - ScopeTest::new(&test_env) - .test(req_gen, write_project) - .await - .unwrap(); - - // Approve beta as private so we can schedule it - let req = test::TestRequest::patch() - .uri(&format!("/v3/project/{beta_project_id}")) - .append_header(("Authorization", MOD_USER_PAT)) - .set_json(json!({ - "status": "private" - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - let req_gen = || { - test::TestRequest::post() - .uri(&format!("/v3/project/{beta_project_id}/schedule")) // beta_project_id is an unpublished can schedule it - .set_json(json!( - { - "requested_status": "private", - "time": Utc::now() + Duration::days(1), - } - )) - }; - ScopeTest::new(&test_env) - .test(req_gen, write_project) - .await - .unwrap(); - - // Icons and gallery images - let req_gen = || { - test::TestRequest::patch() - .uri(&format!("/v3/project/{beta_project_id}/icon?ext=png")) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] - )) - }; - ScopeTest::new(&test_env) - .test(req_gen, write_project) - .await - .unwrap(); - - let req_gen = - || test::TestRequest::delete().uri(&format!("/v3/project/{beta_project_id}/icon")); - ScopeTest::new(&test_env) - .test(req_gen, write_project) - .await - .unwrap(); - - let req_gen = || { - test::TestRequest::post() - .uri(&format!( - "/v3/project/{beta_project_id}/gallery?ext=png&featured=true" - )) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] - )) - }; - ScopeTest::new(&test_env) - .test(req_gen, write_project) - .await - .unwrap(); - - // Get project, as we need the gallery image url - let req_gen = test::TestRequest::get() - .uri(&format!("/v3/project/{beta_project_id}")) - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - let resp = test_env.call(req_gen).await; - let project: serde_json::Value = test::read_body_json(resp).await; - let gallery_url = project["gallery"][0]["url"].as_str().unwrap(); - - let req_gen = || { - test::TestRequest::patch().uri(&format!( - "/v3/project/{beta_project_id}/gallery?url={gallery_url}" - )) - }; - ScopeTest::new(&test_env) - .test(req_gen, write_project) - .await - .unwrap(); - - let req_gen = || { - test::TestRequest::delete().uri(&format!( - "/v3/project/{beta_project_id}/gallery?url={gallery_url}" - )) - }; - ScopeTest::new(&test_env) - .test(req_gen, write_project) - .await - .unwrap(); - - // Team scopes - add user 'friend' - let req_gen = || { - test::TestRequest::post() - .uri(&format!("/v3/team/{alpha_team_id}/members")) + .append_header(("Authorization", MOD_USER_PAT)) .set_json(json!({ - "user_id": FRIEND_USER_ID + "status": "private" })) - }; - ScopeTest::new(&test_env) - .test(req_gen, write_project) - .await - .unwrap(); - - // Accept team invite as 'friend' - let req_gen = || test::TestRequest::post().uri(&format!("/v3/team/{alpha_team_id}/join")); - ScopeTest::new(&test_env) - .with_user_id(FRIEND_USER_ID_PARSED) - .test(req_gen, write_project) - .await - .unwrap(); - - // Patch 'friend' user - let req_gen = || { - test::TestRequest::patch() - .uri(&format!( - "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + let req_gen = || { + test::TestRequest::post() + .uri(&format!("/v3/project/{beta_project_id}/schedule")) // beta_project_id is an unpublished can schedule it + .set_json(json!( + { + "requested_status": "private", + "time": Utc::now() + Duration::days(1), + } + )) + }; + ScopeTest::new(&test_env) + .test(req_gen, write_project) + .await + .unwrap(); + + // Icons and gallery images + let req_gen = || { + test::TestRequest::patch() + .uri(&format!("/v3/project/{beta_project_id}/icon?ext=png")) + .set_payload(Bytes::from( + include_bytes!("../tests/files/200x200.png") as &[u8] + )) + }; + ScopeTest::new(&test_env) + .test(req_gen, write_project) + .await + .unwrap(); + + let req_gen = + || test::TestRequest::delete().uri(&format!("/v3/project/{beta_project_id}/icon")); + ScopeTest::new(&test_env) + .test(req_gen, write_project) + .await + .unwrap(); + + let req_gen = || { + test::TestRequest::post() + .uri(&format!( + "/v3/project/{beta_project_id}/gallery?ext=png&featured=true" + )) + .set_payload(Bytes::from( + include_bytes!("../tests/files/200x200.png") as &[u8] + )) + }; + ScopeTest::new(&test_env) + .test(req_gen, write_project) + .await + .unwrap(); + + // Get project, as we need the gallery image url + let req_gen = test::TestRequest::get() + .uri(&format!("/v3/project/{beta_project_id}")) + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + let resp = test_env.call(req_gen).await; + let project: serde_json::Value = test::read_body_json(resp).await; + let gallery_url = project["gallery"][0]["url"].as_str().unwrap(); + + let req_gen = || { + test::TestRequest::patch().uri(&format!( + "/v3/project/{beta_project_id}/gallery?url={gallery_url}" )) - .set_json(json!({ - "permissions": 1 - })) - }; - ScopeTest::new(&test_env) - .test(req_gen, write_project) - .await - .unwrap(); - - // Transfer ownership to 'friend' - let req_gen = || { - test::TestRequest::patch() - .uri(&format!("/v3/team/{alpha_team_id}/owner")) - .set_json(json!({ - "user_id": FRIEND_USER_ID - })) - }; - ScopeTest::new(&test_env) - .test(req_gen, write_project) - .await - .unwrap(); - - // Now as 'friend', delete 'user' - let req_gen = || { - test::TestRequest::delete().uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) - }; - ScopeTest::new(&test_env) - .with_user_id(FRIEND_USER_ID_PARSED) - .test(req_gen, write_project) - .await - .unwrap(); - - // Delete project - // TODO: this route is currently broken, - // because the Project::get_id contained within Project::remove doesnt include hidden versions, meaning that if there - // is a hidden version, it will fail to delete the project (with a 500 error, as the versions of a project are not all deleted) - // let delete_version = Scopes::PROJECT_DELETE; - // let req_gen = || { - // test::TestRequest::delete() - // .uri(&format!("/v3/project/{beta_project_id}")) - // }; - // ScopeTest::new(&test_env).test(req_gen, delete_version).await.unwrap(); - - // Cleanup test db - test_env.cleanup().await; + }; + ScopeTest::new(&test_env) + .test(req_gen, write_project) + .await + .unwrap(); + + let req_gen = || { + test::TestRequest::delete().uri(&format!( + "/v3/project/{beta_project_id}/gallery?url={gallery_url}" + )) + }; + ScopeTest::new(&test_env) + .test(req_gen, write_project) + .await + .unwrap(); + + // Team scopes - add user 'friend' + let req_gen = || { + test::TestRequest::post() + .uri(&format!("/v3/team/{alpha_team_id}/members")) + .set_json(json!({ + "user_id": FRIEND_USER_ID + })) + }; + ScopeTest::new(&test_env) + .test(req_gen, write_project) + .await + .unwrap(); + + // Accept team invite as 'friend' + let req_gen = || test::TestRequest::post().uri(&format!("/v3/team/{alpha_team_id}/join")); + ScopeTest::new(&test_env) + .with_user_id(FRIEND_USER_ID_PARSED) + .test(req_gen, write_project) + .await + .unwrap(); + + // Patch 'friend' user + let req_gen = || { + test::TestRequest::patch() + .uri(&format!( + "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + )) + .set_json(json!({ + "permissions": 1 + })) + }; + ScopeTest::new(&test_env) + .test(req_gen, write_project) + .await + .unwrap(); + + // Transfer ownership to 'friend' + let req_gen = || { + test::TestRequest::patch() + .uri(&format!("/v3/team/{alpha_team_id}/owner")) + .set_json(json!({ + "user_id": FRIEND_USER_ID + })) + }; + ScopeTest::new(&test_env) + .test(req_gen, write_project) + .await + .unwrap(); + + // Now as 'friend', delete 'user' + let req_gen = || { + test::TestRequest::delete() + .uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) + }; + ScopeTest::new(&test_env) + .with_user_id(FRIEND_USER_ID_PARSED) + .test(req_gen, write_project) + .await + .unwrap(); + + // Delete project + // TODO: this route is currently broken, + // because the Project::get_id contained within Project::remove doesnt include hidden versions, meaning that if there + // is a hidden version, it will fail to delete the project (with a 500 error, as the versions of a project are not all deleted) + // let delete_version = Scopes::PROJECT_DELETE; + // let req_gen = || { + // test::TestRequest::delete() + // .uri(&format!("/v3/project/{beta_project_id}")) + // }; + // ScopeTest::new(&test_env).test(req_gen, delete_version).await.unwrap(); + }) + .await; } // Version write #[actix_rt::test] pub async fn version_write_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let alpha_version_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_alpha - .version_id - .clone(); - let beta_version_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_beta - .version_id - .clone(); - let alpha_file_hash = &test_env - .dummy - .as_ref() - .unwrap() - .project_alpha - .file_hash - .clone(); - - let write_version = Scopes::VERSION_WRITE; - - // Approve beta version as private so we can schedule it - let req = test::TestRequest::patch() - .uri(&format!("/v3/version/{beta_version_id}")) - .append_header(("Authorization", MOD_USER_PAT)) - .set_json(json!({ - "status": "unlisted" - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // Schedule version - let req_gen = || { - test::TestRequest::post() - .uri(&format!("/v3/version/{beta_version_id}/schedule")) // beta_version_id is an *approved* version, so we can schedule it - .set_json(json!( - { - "requested_status": "archived", - "time": Utc::now() + Duration::days(1), - } - )) - }; - ScopeTest::new(&test_env) - .test(req_gen, write_version) - .await - .unwrap(); - - // Patch version - let req_gen = || { - test::TestRequest::patch() - .uri(&format!("/v3/version/{alpha_version_id}")) - .set_json(json!( - { - "name": "test_version_write_scopes Title", - } - )) - }; - ScopeTest::new(&test_env) - .test(req_gen, write_version) - .await - .unwrap(); - - // Generate test project data. - // Basic json - let json_segment = MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text( - serde_json::to_string(&json!( - { - "file_types": { - "simple-zip.zip": "required-resource-pack" - }, - } - )) - .unwrap(), - ), - }; - - // Differently named file, with different content - let content_segment = MultipartSegment { - name: "simple-zip.zip".to_string(), - filename: Some("simple-zip.zip".to_string()), - content_type: Some("application/zip".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../tests/files/simple-zip.zip").to_vec(), - ), - }; - - // Upload version file - let req_gen = || { - test::TestRequest::post() - .uri(&format!("/v3/version/{alpha_version_id}/file")) - .set_multipart(vec![json_segment.clone(), content_segment.clone()]) - }; - ScopeTest::new(&test_env) - .test(req_gen, write_version) - .await - .unwrap(); - - // Delete version file - // TODO: Should this scope be VERSION_DELETE? - let req_gen = || { - test::TestRequest::delete().uri(&format!("/v3/version_file/{alpha_file_hash}")) - // Delete from alpha_version_id, as we uploaded to alpha_version_id and it needs another file - }; - ScopeTest::new(&test_env) - .test(req_gen, write_version) - .await - .unwrap(); - - // Delete version - let delete_version = Scopes::VERSION_DELETE; - let req_gen = || test::TestRequest::delete().uri(&format!("/v3/version/{alpha_version_id}")); - ScopeTest::new(&test_env) - .test(req_gen, delete_version) - .await - .unwrap(); - - // Cleanup test db - test_env.cleanup().await; + with_test_environment_all(None, |test_env| async move { + let alpha_version_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .version_id + .clone(); + let beta_version_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .version_id + .clone(); + let alpha_file_hash = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .file_hash + .clone(); + + let write_version = Scopes::VERSION_WRITE; + + // Approve beta version as private so we can schedule it + let req = test::TestRequest::patch() + .uri(&format!("/v3/version/{beta_version_id}")) + .append_header(("Authorization", MOD_USER_PAT)) + .set_json(json!({ + "status": "unlisted" + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // Schedule version + let req_gen = || { + test::TestRequest::post() + .uri(&format!("/v3/version/{beta_version_id}/schedule")) // beta_version_id is an *approved* version, so we can schedule it + .set_json(json!( + { + "requested_status": "archived", + "time": Utc::now() + Duration::days(1), + } + )) + }; + ScopeTest::new(&test_env) + .test(req_gen, write_version) + .await + .unwrap(); + + // Patch version + let req_gen = || { + test::TestRequest::patch() + .uri(&format!("/v3/version/{alpha_version_id}")) + .set_json(json!( + { + "name": "test_version_write_scopes Title", + } + )) + }; + ScopeTest::new(&test_env) + .test(req_gen, write_version) + .await + .unwrap(); + + // Generate test project data. + // Basic json + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text( + serde_json::to_string(&json!( + { + "file_types": { + "simple-zip.zip": "required-resource-pack" + }, + } + )) + .unwrap(), + ), + }; + + // Differently named file, with different content + let content_segment = MultipartSegment { + name: "simple-zip.zip".to_string(), + filename: Some("simple-zip.zip".to_string()), + content_type: Some("application/zip".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../tests/files/simple-zip.zip").to_vec(), + ), + }; + + // Upload version file + let req_gen = || { + test::TestRequest::post() + .uri(&format!("/v3/version/{alpha_version_id}/file")) + .set_multipart(vec![json_segment.clone(), content_segment.clone()]) + }; + ScopeTest::new(&test_env) + .test(req_gen, write_version) + .await + .unwrap(); + + // Delete version file + // TODO: Should this scope be VERSION_DELETE? + let req_gen = || { + test::TestRequest::delete().uri(&format!("/v3/version_file/{alpha_file_hash}")) + // Delete from alpha_version_id, as we uploaded to alpha_version_id and it needs another file + }; + ScopeTest::new(&test_env) + .test(req_gen, write_version) + .await + .unwrap(); + + // Delete version + let delete_version = Scopes::VERSION_DELETE; + let req_gen = + || test::TestRequest::delete().uri(&format!("/v3/version/{alpha_version_id}")); + ScopeTest::new(&test_env) + .test(req_gen, delete_version) + .await + .unwrap(); + }) + .await; } // Report scopes #[actix_rt::test] pub async fn report_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let beta_project_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_beta - .project_id - .clone(); - - // Create report - let report_create = Scopes::REPORT_CREATE; - let req_gen = || { - test::TestRequest::post().uri("/v3/report").set_json(json!({ - "report_type": "copyright", - "item_id": beta_project_id, - "item_type": "project", - "body": "This is a reupload of my mod, ", - })) - }; - ScopeTest::new(&test_env) - .test(req_gen, report_create) - .await - .unwrap(); - - // Get reports - let report_read = Scopes::REPORT_READ; - let req_gen = || test::TestRequest::get().uri("/v3/report"); - let (_, success) = ScopeTest::new(&test_env) - .test(req_gen, report_read) - .await - .unwrap(); - let report_id = success[0]["id"].as_str().unwrap(); - - let req_gen = || test::TestRequest::get().uri(&format!("/v3/report/{}", report_id)); - ScopeTest::new(&test_env) - .test(req_gen, report_read) - .await - .unwrap(); - - let req_gen = || { - test::TestRequest::get().uri(&format!( - "/v3/reports?ids=[{}]", - urlencoding::encode(&format!("\"{}\"", report_id)) - )) - }; - ScopeTest::new(&test_env) - .test(req_gen, report_read) - .await - .unwrap(); - - // Edit report - let report_edit = Scopes::REPORT_WRITE; - let req_gen = || { - test::TestRequest::patch() - .uri(&format!("/v3/report/{}", report_id)) - .set_json(json!({ - "body": "This is a reupload of my mod, G8!", + with_test_environment_all(None, |test_env| async move { + let beta_project_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .project_id + .clone(); + + // Create report + let report_create = Scopes::REPORT_CREATE; + let req_gen = || { + test::TestRequest::post().uri("/v3/report").set_json(json!({ + "report_type": "copyright", + "item_id": beta_project_id, + "item_type": "project", + "body": "This is a reupload of my mod, ", })) - }; - ScopeTest::new(&test_env) - .test(req_gen, report_edit) - .await - .unwrap(); - - // Delete report - // We use a moderator PAT here, as only moderators can delete reports - let report_delete = Scopes::REPORT_DELETE; - let req_gen = || test::TestRequest::delete().uri(&format!("/v3/report/{}", report_id)); - ScopeTest::new(&test_env) - .with_user_id(MOD_USER_ID_PARSED) - .test(req_gen, report_delete) - .await - .unwrap(); - - // Cleanup test db - test_env.cleanup().await; + }; + ScopeTest::new(&test_env) + .test(req_gen, report_create) + .await + .unwrap(); + + // Get reports + let report_read = Scopes::REPORT_READ; + let req_gen = || test::TestRequest::get().uri("/v3/report"); + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, report_read) + .await + .unwrap(); + let report_id = success[0]["id"].as_str().unwrap(); + + let req_gen = || test::TestRequest::get().uri(&format!("/v3/report/{}", report_id)); + ScopeTest::new(&test_env) + .test(req_gen, report_read) + .await + .unwrap(); + + let req_gen = || { + test::TestRequest::get().uri(&format!( + "/v3/reports?ids=[{}]", + urlencoding::encode(&format!("\"{}\"", report_id)) + )) + }; + ScopeTest::new(&test_env) + .test(req_gen, report_read) + .await + .unwrap(); + + // Edit report + let report_edit = Scopes::REPORT_WRITE; + let req_gen = || { + test::TestRequest::patch() + .uri(&format!("/v3/report/{}", report_id)) + .set_json(json!({ + "body": "This is a reupload of my mod, G8!", + })) + }; + ScopeTest::new(&test_env) + .test(req_gen, report_edit) + .await + .unwrap(); + + // Delete report + // We use a moderator PAT here, as only moderators can delete reports + let report_delete = Scopes::REPORT_DELETE; + let req_gen = || test::TestRequest::delete().uri(&format!("/v3/report/{}", report_id)); + ScopeTest::new(&test_env) + .with_user_id(MOD_USER_ID_PARSED) + .test(req_gen, report_delete) + .await + .unwrap(); + }) + .await; } // Thread scopes #[actix_rt::test] pub async fn thread_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let alpha_thread_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_alpha - .thread_id - .clone(); - let beta_thread_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_beta - .thread_id - .clone(); - - // Thread read - let thread_read = Scopes::THREAD_READ; - let req_gen = || test::TestRequest::get().uri(&format!("/v3/thread/{alpha_thread_id}")); - ScopeTest::new(&test_env) - .test(req_gen, thread_read) - .await - .unwrap(); - - let req_gen = || { - test::TestRequest::get().uri(&format!( - "/v3/threads?ids=[{}]", - urlencoding::encode(&format!("\"{}\"", "U")) - )) - }; - ScopeTest::new(&test_env) - .test(req_gen, thread_read) - .await - .unwrap(); - - // Thread write (to also push to moderator inbox) - let thread_write = Scopes::THREAD_WRITE; - let req_gen = || { - test::TestRequest::post() - .uri(&format!("/v3/thread/{beta_thread_id}")) - .set_json(json!({ - "body": { - "type": "text", - "body": "test_thread_scopes Body" - } - })) - }; - ScopeTest::new(&test_env) - .with_user_id(USER_USER_ID_PARSED) - .test(req_gen, thread_write) - .await - .unwrap(); - - // Check moderation inbox - // Uses moderator PAT, as only moderators can see the moderation inbox - let req_gen = || test::TestRequest::get().uri("/v3/thread/inbox"); - let (_, success) = ScopeTest::new(&test_env) - .with_user_id(MOD_USER_ID_PARSED) - .test(req_gen, thread_read) - .await - .unwrap(); - let thread_id: &str = success[0]["id"].as_str().unwrap(); - - // Moderator 'read' thread - // Uses moderator PAT, as only moderators can see the moderation inbox - let req_gen = || test::TestRequest::post().uri(&format!("/v3/thread/{thread_id}/read")); - ScopeTest::new(&test_env) - .with_user_id(MOD_USER_ID_PARSED) - .test(req_gen, thread_read) - .await - .unwrap(); - - // Delete that message - // First, get message id - let req_gen = test::TestRequest::get() - .uri(&format!("/v3/thread/{thread_id}")) - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - let resp = test_env.call(req_gen).await; - let success: serde_json::Value = test::read_body_json(resp).await; - let thread_message_id = success["messages"][0]["id"].as_str().unwrap(); - - let req_gen = || test::TestRequest::delete().uri(&format!("/v3/message/{thread_message_id}")); - ScopeTest::new(&test_env) - .with_user_id(MOD_USER_ID_PARSED) - .test(req_gen, thread_write) - .await - .unwrap(); - - // Cleanup test db - test_env.cleanup().await; + with_test_environment_all(None, |test_env| async move { + let alpha_thread_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .thread_id + .clone(); + let beta_thread_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .thread_id + .clone(); + + // Thread read + let thread_read = Scopes::THREAD_READ; + let req_gen = || test::TestRequest::get().uri(&format!("/v3/thread/{alpha_thread_id}")); + ScopeTest::new(&test_env) + .test(req_gen, thread_read) + .await + .unwrap(); + + let req_gen = || { + test::TestRequest::get().uri(&format!( + "/v3/threads?ids=[{}]", + urlencoding::encode(&format!("\"{}\"", "U")) + )) + }; + ScopeTest::new(&test_env) + .test(req_gen, thread_read) + .await + .unwrap(); + + // Thread write (to also push to moderator inbox) + let thread_write = Scopes::THREAD_WRITE; + let req_gen = || { + test::TestRequest::post() + .uri(&format!("/v3/thread/{beta_thread_id}")) + .set_json(json!({ + "body": { + "type": "text", + "body": "test_thread_scopes Body" + } + })) + }; + ScopeTest::new(&test_env) + .with_user_id(USER_USER_ID_PARSED) + .test(req_gen, thread_write) + .await + .unwrap(); + + // Check moderation inbox + // Uses moderator PAT, as only moderators can see the moderation inbox + let req_gen = || test::TestRequest::get().uri("/v3/thread/inbox"); + let (_, success) = ScopeTest::new(&test_env) + .with_user_id(MOD_USER_ID_PARSED) + .test(req_gen, thread_read) + .await + .unwrap(); + let thread_id: &str = success[0]["id"].as_str().unwrap(); + + // Moderator 'read' thread + // Uses moderator PAT, as only moderators can see the moderation inbox + let req_gen = || test::TestRequest::post().uri(&format!("/v3/thread/{thread_id}/read")); + ScopeTest::new(&test_env) + .with_user_id(MOD_USER_ID_PARSED) + .test(req_gen, thread_read) + .await + .unwrap(); + + // Delete that message + // First, get message id + let req_gen = test::TestRequest::get() + .uri(&format!("/v3/thread/{thread_id}")) + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + let resp = test_env.call(req_gen).await; + let success: serde_json::Value = test::read_body_json(resp).await; + let thread_message_id = success["messages"][0]["id"].as_str().unwrap(); + + let req_gen = + || test::TestRequest::delete().uri(&format!("/v3/message/{thread_message_id}")); + ScopeTest::new(&test_env) + .with_user_id(MOD_USER_ID_PARSED) + .test(req_gen, thread_write) + .await + .unwrap(); + }) + .await; } // Pat scopes #[actix_rt::test] pub async fn pat_scopes() { - let test_env = TestEnvironment::build(None).await; - - // Pat create - let pat_create = Scopes::PAT_CREATE; - let req_gen = || { - test::TestRequest::post().uri("/v3/pat").set_json(json!({ - "scopes": 1, - "name": "test_pat_scopes Name", - "expires": Utc::now() + Duration::days(1), - })) - }; - let (_, success) = ScopeTest::new(&test_env) - .test(req_gen, pat_create) - .await - .unwrap(); - let pat_id = success["id"].as_str().unwrap(); - - // Pat write - let pat_write = Scopes::PAT_WRITE; - let req_gen = || { - test::TestRequest::patch() - .uri(&format!("/v3/pat/{pat_id}")) - .set_json(json!({})) - }; - ScopeTest::new(&test_env) - .test(req_gen, pat_write) - .await - .unwrap(); - - // Pat read - let pat_read = Scopes::PAT_READ; - let req_gen = || test::TestRequest::get().uri("/v3/pat"); - ScopeTest::new(&test_env) - .test(req_gen, pat_read) - .await - .unwrap(); - - // Pat delete - let pat_delete = Scopes::PAT_DELETE; - let req_gen = || test::TestRequest::delete().uri(&format!("/v3/pat/{pat_id}")); - ScopeTest::new(&test_env) - .test(req_gen, pat_delete) - .await - .unwrap(); - - // Cleanup test db - test_env.cleanup().await; + with_test_environment_all(None, |test_env| async move { + // Pat create + let pat_create = Scopes::PAT_CREATE; + let req_gen = || { + test::TestRequest::post().uri("/v3/pat").set_json(json!({ + "scopes": 1, + "name": "test_pat_scopes Name", + "expires": Utc::now() + Duration::days(1), + })) + }; + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, pat_create) + .await + .unwrap(); + let pat_id = success["id"].as_str().unwrap(); + + // Pat write + let pat_write = Scopes::PAT_WRITE; + let req_gen = || { + test::TestRequest::patch() + .uri(&format!("/v3/pat/{pat_id}")) + .set_json(json!({})) + }; + ScopeTest::new(&test_env) + .test(req_gen, pat_write) + .await + .unwrap(); + + // Pat read + let pat_read = Scopes::PAT_READ; + let req_gen = || test::TestRequest::get().uri("/v3/pat"); + ScopeTest::new(&test_env) + .test(req_gen, pat_read) + .await + .unwrap(); + + // Pat delete + let pat_delete = Scopes::PAT_DELETE; + let req_gen = || test::TestRequest::delete().uri(&format!("/v3/pat/{pat_id}")); + ScopeTest::new(&test_env) + .test(req_gen, pat_delete) + .await + .unwrap(); + }) + .await; } // Collection scopes #[actix_rt::test] pub async fn collections_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let alpha_project_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_alpha - .project_id - .clone(); - - // Create collection - let collection_create = Scopes::COLLECTION_CREATE; - let req_gen = || { - test::TestRequest::post() - .uri("/v3/collection") - .set_json(json!({ - "title": "Test Collection", - "description": "Test Collection Description", - "projects": [alpha_project_id] - })) - }; - let (_, success) = ScopeTest::new(&test_env) - .test(req_gen, collection_create) - .await - .unwrap(); - let collection_id = success["id"].as_str().unwrap(); - - // Patch collection - // Collections always initialize to public, so we do patch before Get testing - let collection_write = Scopes::COLLECTION_WRITE; - let req_gen = || { - test::TestRequest::patch() - .uri(&format!("/v3/collection/{collection_id}")) - .set_json(json!({ - "title": "Test Collection patch", - "status": "private", - })) - }; - ScopeTest::new(&test_env) - .test(req_gen, collection_write) - .await - .unwrap(); - - // Read collection - let collection_read = Scopes::COLLECTION_READ; - let req_gen = || test::TestRequest::get().uri(&format!("/v3/collection/{}", collection_id)); - ScopeTest::new(&test_env) - .with_failure_code(404) - .test(req_gen, collection_read) - .await - .unwrap(); - - let req_gen = || { - test::TestRequest::get().uri(&format!( - "/v3/collections?ids=[{}]", - urlencoding::encode(&format!("\"{}\"", collection_id)) - )) - }; - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .test(req_gen, collection_read) - .await - .unwrap(); - assert_eq!(failure.as_array().unwrap().len(), 0); - assert_eq!(success.as_array().unwrap().len(), 1); - - let req_gen = || test::TestRequest::get().uri(&format!("/v3/user/{USER_USER_ID}/collections")); - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .test(req_gen, collection_read) - .await - .unwrap(); - assert_eq!(failure.as_array().unwrap().len(), 0); - assert_eq!(success.as_array().unwrap().len(), 1); - - let req_gen = || { - test::TestRequest::patch() - .uri(&format!("/v3/collection/{collection_id}/icon?ext=png")) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] + with_test_environment_all(None, |test_env| async move { + let alpha_project_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .project_id + .clone(); + + // Create collection + let collection_create = Scopes::COLLECTION_CREATE; + let req_gen = || { + test::TestRequest::post() + .uri("/v3/collection") + .set_json(json!({ + "title": "Test Collection", + "description": "Test Collection Description", + "projects": [alpha_project_id] + })) + }; + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, collection_create) + .await + .unwrap(); + let collection_id = success["id"].as_str().unwrap(); + + // Patch collection + // Collections always initialize to public, so we do patch before Get testing + let collection_write = Scopes::COLLECTION_WRITE; + let req_gen = || { + test::TestRequest::patch() + .uri(&format!("/v3/collection/{collection_id}")) + .set_json(json!({ + "title": "Test Collection patch", + "status": "private", + })) + }; + ScopeTest::new(&test_env) + .test(req_gen, collection_write) + .await + .unwrap(); + + // Read collection + let collection_read = Scopes::COLLECTION_READ; + let req_gen = || test::TestRequest::get().uri(&format!("/v3/collection/{}", collection_id)); + ScopeTest::new(&test_env) + .with_failure_code(404) + .test(req_gen, collection_read) + .await + .unwrap(); + + let req_gen = || { + test::TestRequest::get().uri(&format!( + "/v3/collections?ids=[{}]", + urlencoding::encode(&format!("\"{}\"", collection_id)) )) - }; - ScopeTest::new(&test_env) - .test(req_gen, collection_write) - .await - .unwrap(); - - let req_gen = - || test::TestRequest::delete().uri(&format!("/v3/collection/{collection_id}/icon")); - ScopeTest::new(&test_env) - .test(req_gen, collection_write) - .await - .unwrap(); - - // Cleanup test db - test_env.cleanup().await; + }; + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .test(req_gen, collection_read) + .await + .unwrap(); + assert_eq!(failure.as_array().unwrap().len(), 0); + assert_eq!(success.as_array().unwrap().len(), 1); + + let req_gen = + || test::TestRequest::get().uri(&format!("/v3/user/{USER_USER_ID}/collections")); + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .test(req_gen, collection_read) + .await + .unwrap(); + assert_eq!(failure.as_array().unwrap().len(), 0); + assert_eq!(success.as_array().unwrap().len(), 1); + + let req_gen = || { + test::TestRequest::patch() + .uri(&format!("/v3/collection/{collection_id}/icon?ext=png")) + .set_payload(Bytes::from( + include_bytes!("../tests/files/200x200.png") as &[u8] + )) + }; + ScopeTest::new(&test_env) + .test(req_gen, collection_write) + .await + .unwrap(); + + let req_gen = + || test::TestRequest::delete().uri(&format!("/v3/collection/{collection_id}/icon")); + ScopeTest::new(&test_env) + .test(req_gen, collection_write) + .await + .unwrap(); + }) + .await; } // Organization scopes (and a couple PROJECT_WRITE scopes that are only allowed for orgs) #[actix_rt::test] pub async fn organization_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let beta_project_id = &test_env - .dummy - .as_ref() - .unwrap() - .project_beta - .project_id - .clone(); - - // Create organization - let organization_create = Scopes::ORGANIZATION_CREATE; - let req_gen = || { - test::TestRequest::post() - .uri("/v3/organization") - .set_json(json!({ - "title": "TestOrg", - "description": "TestOrg Description", - })) - }; - let (_, success) = ScopeTest::new(&test_env) - .test(req_gen, organization_create) - .await - .unwrap(); - let organization_id = success["id"].as_str().unwrap(); - - // Patch organization - let organization_edit = Scopes::ORGANIZATION_WRITE; - let req_gen = || { - test::TestRequest::patch() - .uri(&format!("/v3/organization/{organization_id}")) - .set_json(json!({ - "description": "TestOrg Patch Description", - })) - }; - ScopeTest::new(&test_env) - .test(req_gen, organization_edit) - .await - .unwrap(); - - let req_gen = || { - test::TestRequest::patch() - .uri(&format!("/v3/organization/{organization_id}/icon?ext=png")) - .set_payload(Bytes::from( - include_bytes!("../tests/files/200x200.png") as &[u8] + with_test_environment_all(None, |test_env| async move { + let beta_project_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .project_id + .clone(); + + // Create organization + let organization_create = Scopes::ORGANIZATION_CREATE; + let req_gen = || { + test::TestRequest::post() + .uri("/v3/organization") + .set_json(json!({ + "title": "TestOrg", + "description": "TestOrg Description", + })) + }; + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, organization_create) + .await + .unwrap(); + let organization_id = success["id"].as_str().unwrap(); + + // Patch organization + let organization_edit = Scopes::ORGANIZATION_WRITE; + let req_gen = || { + test::TestRequest::patch() + .uri(&format!("/v3/organization/{organization_id}")) + .set_json(json!({ + "description": "TestOrg Patch Description", + })) + }; + ScopeTest::new(&test_env) + .test(req_gen, organization_edit) + .await + .unwrap(); + + let req_gen = || { + test::TestRequest::patch() + .uri(&format!("/v3/organization/{organization_id}/icon?ext=png")) + .set_payload(Bytes::from( + include_bytes!("../tests/files/200x200.png") as &[u8] + )) + }; + ScopeTest::new(&test_env) + .test(req_gen, organization_edit) + .await + .unwrap(); + + let req_gen = + || test::TestRequest::delete().uri(&format!("/v3/organization/{organization_id}/icon")); + ScopeTest::new(&test_env) + .test(req_gen, organization_edit) + .await + .unwrap(); + + // add project + let organization_project_edit = Scopes::PROJECT_WRITE | Scopes::ORGANIZATION_WRITE; + let req_gen = || { + test::TestRequest::post() + .uri(&format!("/v3/organization/{organization_id}/projects")) + .set_json(json!({ + "project_id": beta_project_id + })) + }; + ScopeTest::new(&test_env) + .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_WRITE) + .test(req_gen, organization_project_edit) + .await + .unwrap(); + + // Organization reads + let organization_read = Scopes::ORGANIZATION_READ; + let req_gen = + || test::TestRequest::get().uri(&format!("/v3/organization/{organization_id}")); + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .test(req_gen, organization_read) + .await + .unwrap(); + assert!(failure["members"][0]["permissions"].is_null()); + assert!(!success["members"][0]["permissions"].is_null()); + + let req_gen = || { + test::TestRequest::get().uri(&format!( + "/v3/organizations?ids=[{}]", + urlencoding::encode(&format!("\"{}\"", organization_id)) )) - }; - ScopeTest::new(&test_env) - .test(req_gen, organization_edit) - .await - .unwrap(); - - let req_gen = - || test::TestRequest::delete().uri(&format!("/v3/organization/{organization_id}/icon")); - ScopeTest::new(&test_env) - .test(req_gen, organization_edit) - .await - .unwrap(); - - // add project - let organization_project_edit = Scopes::PROJECT_WRITE | Scopes::ORGANIZATION_WRITE; - let req_gen = || { - test::TestRequest::post() - .uri(&format!("/v3/organization/{organization_id}/projects")) - .set_json(json!({ - "project_id": beta_project_id - })) - }; - ScopeTest::new(&test_env) - .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_WRITE) - .test(req_gen, organization_project_edit) - .await - .unwrap(); - - // Organization reads - let organization_read = Scopes::ORGANIZATION_READ; - let req_gen = || test::TestRequest::get().uri(&format!("/v3/organization/{organization_id}")); - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .test(req_gen, organization_read) - .await - .unwrap(); - assert!(failure["members"][0]["permissions"].is_null()); - assert!(!success["members"][0]["permissions"].is_null()); - - let req_gen = || { - test::TestRequest::get().uri(&format!( - "/v3/organizations?ids=[{}]", - urlencoding::encode(&format!("\"{}\"", organization_id)) - )) - }; - - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .test(req_gen, organization_read) - .await - .unwrap(); - assert!(failure[0]["members"][0]["permissions"].is_null()); - assert!(!success[0]["members"][0]["permissions"].is_null()); - - let organization_project_read = Scopes::PROJECT_READ | Scopes::ORGANIZATION_READ; - let req_gen = - || test::TestRequest::get().uri(&format!("/v3/organization/{organization_id}/projects")); - let (failure, success) = ScopeTest::new(&test_env) - .with_failure_code(200) - .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_READ) - .test(req_gen, organization_project_read) - .await - .unwrap(); - assert!(failure.as_array().unwrap().is_empty()); - assert!(!success.as_array().unwrap().is_empty()); - - // remove project (now that we've checked) - let req_gen = || { - test::TestRequest::delete().uri(&format!( - "/v3/organization/{organization_id}/projects/{beta_project_id}" - )) - }; - ScopeTest::new(&test_env) - .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_WRITE) - .test(req_gen, organization_project_edit) - .await - .unwrap(); - - // Delete organization - let organization_delete = Scopes::ORGANIZATION_DELETE; - let req_gen = - || test::TestRequest::delete().uri(&format!("/v3/organization/{organization_id}")); - ScopeTest::new(&test_env) - .test(req_gen, organization_delete) - .await - .unwrap(); - - // Cleanup test db - test_env.cleanup().await; + }; + + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .test(req_gen, organization_read) + .await + .unwrap(); + assert!(failure[0]["members"][0]["permissions"].is_null()); + assert!(!success[0]["members"][0]["permissions"].is_null()); + + let organization_project_read = Scopes::PROJECT_READ | Scopes::ORGANIZATION_READ; + let req_gen = || { + test::TestRequest::get().uri(&format!("/v3/organization/{organization_id}/projects")) + }; + let (failure, success) = ScopeTest::new(&test_env) + .with_failure_code(200) + .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_READ) + .test(req_gen, organization_project_read) + .await + .unwrap(); + assert!(failure.as_array().unwrap().is_empty()); + assert!(!success.as_array().unwrap().is_empty()); + + // remove project (now that we've checked) + let req_gen = || { + test::TestRequest::delete().uri(&format!( + "/v3/organization/{organization_id}/projects/{beta_project_id}" + )) + }; + ScopeTest::new(&test_env) + .with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_WRITE) + .test(req_gen, organization_project_edit) + .await + .unwrap(); + + // Delete organization + let organization_delete = Scopes::ORGANIZATION_DELETE; + let req_gen = + || test::TestRequest::delete().uri(&format!("/v3/organization/{organization_id}")); + ScopeTest::new(&test_env) + .test(req_gen, organization_delete) + .await + .unwrap(); + }) + .await; } // TODO: Analytics scopes diff --git a/tests/search.rs b/tests/search.rs index 120aedd6..e0693125 100644 --- a/tests/search.rs +++ b/tests/search.rs @@ -1,6 +1,9 @@ +use common::api_v3::ApiV3; use common::database::*; use common::dummy_data::TestFile; use common::dummy_data::DUMMY_CATEGORIES; + +use common::environment::with_test_environment; use common::environment::TestEnvironment; use futures::stream::StreamExt; use labrinth::models::ids::base62_impl::parse_base62; @@ -8,9 +11,9 @@ use serde_json::json; use std::collections::HashMap; use std::sync::Arc; -use crate::common::api_v3::request_data::{ - self, get_public_version_creation_data, ProjectCreationRequestData, -}; +use crate::common::api_common::Api; +use crate::common::api_common::ApiProject; +use crate::common::api_common::ApiVersion; mod common; @@ -20,288 +23,289 @@ mod common; #[actix_rt::test] async fn search_projects() { // Test setup and dummy data - let test_env = TestEnvironment::build(Some(10)).await; - let api = &test_env.v3; - let test_name = test_env.db.database_name.clone(); + with_test_environment(Some(10), |test_env: TestEnvironment| async move { + let api = &test_env.api; + let test_name = test_env.db.database_name.clone(); - // Add dummy projects of various categories for searchability - let mut project_creation_futures = vec![]; + // Add dummy projects of various categories for searchability + let mut project_creation_futures = vec![]; - let create_async_future = - |id: u64, - pat: &'static str, - is_modpack: bool, - modify_json: Box| { - let slug = format!("{test_name}-searchable-project-{id}"); + let create_async_future = + |id: u64, + pat: &'static str, + is_modpack: bool, + modify_json: Option| { + let slug = format!("{test_name}-searchable-project-{id}"); - let jar = if is_modpack { - TestFile::build_random_mrpack() - } else { - TestFile::build_random_jar() - }; - let mut basic_project_json = - request_data::get_public_project_creation_data_json(&slug, Some(&jar)); - modify_json(&mut basic_project_json); - let basic_project_multipart = - request_data::get_public_creation_data_multipart(&basic_project_json, Some(&jar)); - // Add a project- simple, should work. - let req = api.add_public_project( - ProjectCreationRequestData { - slug, - jar: Some(jar), - segment_data: basic_project_multipart, - }, - pat, - ); - async move { - let (project, _) = req.await; + let jar = if is_modpack { + TestFile::build_random_mrpack() + } else { + TestFile::build_random_jar() + }; + async move { + // Add a project- simple, should work. + let req = api.add_public_project(&slug, Some(jar), modify_json, pat); + let (project, _) = req.await; - // Approve, so that the project is searchable - let resp = api - .edit_project( - &project.id.to_string(), - json!({ - "status": "approved" - }), - MOD_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - (project.id.0, id) - } - }; + // Approve, so that the project is searchable + let resp = api + .edit_project( + &project.id.to_string(), + json!({ + "status": "approved" + }), + MOD_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + (project.id.0, id) + } + }; - // Test project 0 - let id = 0; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[4..6]); - json["initial_versions"][0]["server_side"] = json!("required"); - json["license_id"] = json!("LGPL-3.0-or-later"); - }; - project_creation_futures.push(create_async_future( - id, - USER_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 0 + let id = 0; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[4..6] }, + { "op": "add", "path": "/initial_versions/0/server_side", "value": "required" }, + { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Some(modify_json), + )); - // Test project 1 - let id = 1; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[0..2]); - json["initial_versions"][0]["client_side"] = json!("optional"); - }; - project_creation_futures.push(create_async_future( - id, - USER_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 1 + let id = 1; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] }, + { "op": "add", "path": "/initial_versions/0/client_side", "value": "optional" }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Some(modify_json), + )); - // Test project 2 - let id = 2; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[0..2]); - json["initial_versions"][0]["server_side"] = json!("required"); - json["title"] = json!("Mysterious Project"); - }; - project_creation_futures.push(create_async_future( - id, - USER_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 2 + let id = 2; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] }, + { "op": "add", "path": "/initial_versions/0/server_side", "value": "required" }, + { "op": "add", "path": "/title", "value": "Mysterious Project" }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Some(modify_json), + )); - // Test project 3 - let id = 3; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[0..3]); - json["initial_versions"][0]["server_side"] = json!("required"); - json["initial_versions"][0]["game_versions"] = json!(["1.20.4"]); - json["title"] = json!("Mysterious Project"); - json["license_id"] = json!("LicenseRef-All-Rights-Reserved"); // closed source - }; - project_creation_futures.push(create_async_future( - id, - FRIEND_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 3 + let id = 3; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] }, + { "op": "add", "path": "/initial_versions/0/server_side", "value": "required" }, + { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.4"] }, + { "op": "add", "path": "/title", "value": "Mysterious Project" }, + { "op": "add", "path": "/license_id", "value": "LicenseRef-All-Rights-Reserved" }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + FRIEND_USER_PAT, + false, + Some(modify_json), + )); - // Test project 4 - let id = 4; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[0..3]); - json["initial_versions"][0]["client_side"] = json!("optional"); - json["initial_versions"][0]["game_versions"] = json!(["1.20.5"]); - }; - project_creation_futures.push(create_async_future( - id, - USER_USER_PAT, - true, - Box::new(modify_json), - )); + // Test project 4 + let id = 4; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] }, + { "op": "add", "path": "/initial_versions/0/client_side", "value": "optional" }, + { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.5"] }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + true, + Some(modify_json), + )); - // Test project 5 - let id = 5; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[5..6]); - json["initial_versions"][0]["client_side"] = json!("optional"); - json["initial_versions"][0]["game_versions"] = json!(["1.20.5"]); - json["license_id"] = json!("LGPL-3.0-or-later"); - }; - project_creation_futures.push(create_async_future( - id, - USER_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 5 + let id = 5; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, + { "op": "add", "path": "/initial_versions/0/client_side", "value": "optional" }, + { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.5"] }, + { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Some(modify_json), + )); - // Test project 6 - let id = 6; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[5..6]); - json["initial_versions"][0]["client_side"] = json!("optional"); - json["initial_versions"][0]["server_side"] = json!("required"); - json["license_id"] = json!("LGPL-3.0-or-later"); - }; - project_creation_futures.push(create_async_future( - id, - FRIEND_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 6 + let id = 6; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, + { "op": "add", "path": "/initial_versions/0/client_side", "value": "optional" }, + { "op": "add", "path": "/initial_versions/0/server_side", "value": "required" }, + { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + FRIEND_USER_PAT, + false, + Some(modify_json), + )); - // Test project 7 (testing the search bug) - // This project has an initial private forge version that is 1.20.3, and a fabric 1.20.5 version. - // This means that a search for fabric + 1.20.3 or forge + 1.20.5 should not return this project. - let id = 7; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[5..6]); - json["initial_versions"][0]["client_side"] = json!("optional"); - json["initial_versions"][0]["server_side"] = json!("required"); - json["license_id"] = json!("LGPL-3.0-or-later"); - json["initial_versions"][0]["loaders"] = json!(["forge"]); - json["initial_versions"][0]["game_versions"] = json!(["1.20.2"]); - }; - project_creation_futures.push(create_async_future( - id, - USER_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 7 (testing the search bug) + // This project has an initial private forge version that is 1.20.3, and a fabric 1.20.5 version. + // This means that a search for fabric + 1.20.3 or forge + 1.20.5 should not return this project. + let id = 7; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, + { "op": "add", "path": "/initial_versions/0/client_side", "value": "optional" }, + { "op": "add", "path": "/initial_versions/0/server_side", "value": "required" }, + { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, + { "op": "add", "path": "/initial_versions/0/loaders", "value": ["forge"] }, + { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.2"] }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Some(modify_json), + )); - // Await all project creation - // Returns a mapping of: - // project id -> test id - let id_conversion: Arc> = Arc::new( - futures::future::join_all(project_creation_futures) - .await - .into_iter() - .collect(), - ); + // Await all project creation + // Returns a mapping of: + // project id -> test id + let id_conversion: Arc> = Arc::new( + futures::future::join_all(project_creation_futures) + .await + .into_iter() + .collect(), + ); - // Create a second version for project 7 - let project_7 = api - .get_project_deserialized(&format!("{test_name}-searchable-project-7"), USER_USER_PAT) - .await; - api.add_public_version( - get_public_version_creation_data( + // Create a second version for project 7 + let project_7 = api + .get_project_deserialized_common( + &format!("{test_name}-searchable-project-7"), + USER_USER_PAT, + ) + .await; + api.add_public_version( project_7.id, "1.0.0", TestFile::build_random_jar(), - None::, - ), - USER_USER_PAT, - ) - .await; - - // Pairs of: - // 1. vec of search facets - // 2. expected project ids to be returned by this search - let pairs = vec![ - (json!([["categories:fabric"]]), vec![0, 1, 2, 3, 4, 5, 6, 7]), - (json!([["categories:forge"]]), vec![7]), - ( - json!([["categories:fabric", "categories:forge"]]), - vec![0, 1, 2, 3, 4, 5, 6, 7], - ), - (json!([["categories:fabric"], ["categories:forge"]]), vec![]), - ( - json!([ - ["categories:fabric"], - [&format!("categories:{}", DUMMY_CATEGORIES[0])], - ]), - vec![1, 2, 3, 4], - ), - (json!([["project_types:modpack"]]), vec![4]), - (json!([["client_side:required"]]), vec![0, 2, 3, 7]), - (json!([["server_side:required"]]), vec![0, 2, 3, 6, 7]), - (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7]), - (json!([["license:MIT"]]), vec![1, 2, 4]), - (json!([[r#"title:'Mysterious Project'"#]]), vec![2, 3]), - (json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7]), - (json!([["game_versions:1.20.5"]]), vec![4, 5]), - // bug fix - ( - json!([ - // Only the forge one has 1.20.2, so its true that this project 'has' - // 1.20.2 and a fabric version, but not true that it has a 1.20.2 fabric version. - ["categories:fabric"], - ["game_versions:1.20.2"] - ]), - vec![], - ), - // Project type change - // Modpack should still be able to search based on former loader, even though technically the loader is 'mrpack' - (json!([["categories:mrpack"]]), vec![4]), - ( - json!([["categories:mrpack"], ["categories:fabric"]]), - vec![4], - ), - ( - json!([ - ["categories:mrpack"], - ["categories:fabric"], - ["project_types:modpack"] - ]), - vec![4], - ), - ]; - // TODO: versions, game versions - // Untested: - // - downloads (not varied) - // - color (not varied) - // - created_timestamp (not varied) - // - modified_timestamp (not varied) - // TODO: multiple different project types test + None, + None, + USER_USER_PAT, + ) + .await; - // Forcibly reset the search index - let resp = api.reset_search_index().await; - assert_eq!(resp.status(), 204); + // Pairs of: + // 1. vec of search facets + // 2. expected project ids to be returned by this search + let pairs = vec![ + (json!([["categories:fabric"]]), vec![0, 1, 2, 3, 4, 5, 6, 7]), + (json!([["categories:forge"]]), vec![7]), + ( + json!([["categories:fabric", "categories:forge"]]), + vec![0, 1, 2, 3, 4, 5, 6, 7], + ), + (json!([["categories:fabric"], ["categories:forge"]]), vec![]), + ( + json!([ + ["categories:fabric"], + [&format!("categories:{}", DUMMY_CATEGORIES[0])], + ]), + vec![1, 2, 3, 4], + ), + (json!([["project_types:modpack"]]), vec![4]), + (json!([["client_side:required"]]), vec![0, 2, 3, 7]), + (json!([["server_side:required"]]), vec![0, 2, 3, 6, 7]), + (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7]), + (json!([["license:MIT"]]), vec![1, 2, 4]), + (json!([[r#"title:'Mysterious Project'"#]]), vec![2, 3]), + (json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7]), + (json!([["game_versions:1.20.5"]]), vec![4, 5]), + // bug fix + ( + json!([ + // Only the forge one has 1.20.2, so its true that this project 'has' + // 1.20.2 and a fabric version, but not true that it has a 1.20.2 fabric version. + ["categories:fabric"], + ["game_versions:1.20.2"] + ]), + vec![], + ), + // Project type change + // Modpack should still be able to search based on former loader, even though technically the loader is 'mrpack' + (json!([["categories:mrpack"]]), vec![4]), + ( + json!([["categories:mrpack"], ["categories:fabric"]]), + vec![4], + ), + ( + json!([ + ["categories:mrpack"], + ["categories:fabric"], + ["project_types:modpack"] + ]), + vec![4], + ), + ]; + // TODO: versions, game versions + // Untested: + // - downloads (not varied) + // - color (not varied) + // - created_timestamp (not varied) + // - modified_timestamp (not varied) + // TODO: multiple different project types test - // Test searches - let stream = futures::stream::iter(pairs); - stream - .for_each_concurrent(1, |(facets, mut expected_project_ids)| { - let id_conversion = id_conversion.clone(); - let test_name = test_name.clone(); - async move { - let projects = api - .search_deserialized(Some(&test_name), Some(facets.clone()), USER_USER_PAT) - .await; - let mut found_project_ids: Vec = projects - .hits - .into_iter() - .map(|p| id_conversion[&parse_base62(&p.project_id).unwrap()]) - .collect(); - expected_project_ids.sort(); - found_project_ids.sort(); - assert_eq!(found_project_ids, expected_project_ids); - } - }) - .await; + // Forcibly reset the search index + let resp = api.reset_search_index().await; + assert_eq!(resp.status(), 204); - // Cleanup test db - test_env.cleanup().await; + // Test searches + let stream = futures::stream::iter(pairs); + stream + .for_each_concurrent(1, |(facets, mut expected_project_ids)| { + let id_conversion = id_conversion.clone(); + let test_name = test_name.clone(); + async move { + let projects = api + .search_deserialized_common( + Some(&test_name), + Some(facets.clone()), + USER_USER_PAT, + ) + .await; + let mut found_project_ids: Vec = projects + .hits + .into_iter() + .map(|p| id_conversion[&parse_base62(&p.project_id).unwrap()]) + .collect(); + expected_project_ids.sort(); + found_project_ids.sort(); + assert_eq!(found_project_ids, expected_project_ids); + } + }) + .await; + }) + .await; } diff --git a/tests/tags.rs b/tests/tags.rs index 3b986a3d..00659443 100644 --- a/tests/tags.rs +++ b/tests/tags.rs @@ -1,44 +1,46 @@ -use common::environment::TestEnvironment; use std::collections::HashSet; +use common::environment::with_test_environment_all; + +use crate::common::api_common::ApiTags; + mod common; #[actix_rt::test] async fn get_tags() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; + with_test_environment_all(None, |test_env| async move { + let api = &test_env.api; + let loaders = api.get_loaders_deserialized_common().await; + let categories = api.get_categories_deserialized_common().await; - let loaders = api.get_loaders_deserialized().await; - let categories = api.get_categories_deserialized().await; + let loader_names = loaders.into_iter().map(|x| x.name).collect::>(); + assert_eq!( + loader_names, + ["fabric", "forge", "mrpack"] + .iter() + .map(|s| s.to_string()) + .collect() + ); - let loader_names = loaders.into_iter().map(|x| x.name).collect::>(); - assert_eq!( - loader_names, - ["fabric", "forge", "mrpack"] + let category_names = categories + .into_iter() + .map(|x| x.name) + .collect::>(); + assert_eq!( + category_names, + [ + "combat", + "economy", + "food", + "optimization", + "decoration", + "mobs", + "magic" + ] .iter() .map(|s| s.to_string()) .collect() - ); - - let category_names = categories - .into_iter() - .map(|x| x.name) - .collect::>(); - assert_eq!( - category_names, - [ - "combat", - "economy", - "food", - "optimization", - "decoration", - "mobs", - "magic" - ] - .iter() - .map(|s| s.to_string()) - .collect() - ); - - test_env.cleanup().await; + ); + }) + .await; } diff --git a/tests/teams.rs b/tests/teams.rs index 74a1d630..929d0bec 100644 --- a/tests/teams.rs +++ b/tests/teams.rs @@ -1,6 +1,6 @@ use crate::common::database::*; -use crate::common::environment::TestEnvironment; use actix_web::test; +use common::environment::with_test_environment_all; use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; use serde_json::json; @@ -9,603 +9,598 @@ mod common; #[actix_rt::test] async fn test_get_team() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; - let zeta_organization_id = &test_env - .dummy - .as_ref() - .unwrap() - .organization_zeta - .organization_id; - let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; - - // Perform tests for an organization team and a project team - for (team_association_id, team_association, team_id) in [ - (alpha_project_id, "project", alpha_team_id), - (zeta_organization_id, "organization", zeta_team_id), - ] { - // A non-member of the team should get basic info but not be able to see private data - for uri in [ - format!("/v3/team/{team_id}/members"), - format!("/v3/{team_association}/{team_association_id}/members"), + with_test_environment_all(None, |test_env| async move { + let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + // Perform tests for an organization team and a project team + for (team_association_id, team_association, team_id) in [ + (alpha_project_id, "project", alpha_team_id), + (zeta_organization_id, "organization", zeta_team_id), ] { - let req = test::TestRequest::get() - .uri(&uri) + // A non-member of the team should get basic info but not be able to see private data + for uri in [ + format!("/v3/team/{team_id}/members"), + format!("/v3/{team_association}/{team_association_id}/members"), + ] { + let req = test::TestRequest::get() + .uri(&uri) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + assert_eq!(value[0]["user"]["id"], USER_USER_ID); + assert!(value[0]["permissions"].is_null()); + } + + // A non-accepted member of the team should: + // - not be able to see private data about the team, but see all members including themselves + // - should not appear in the team members list to enemy users + let req = test::TestRequest::post() + .uri(&format!("/v3/team/{team_id}/members")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(&json!({ + "user_id": FRIEND_USER_ID, + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + for uri in [ + format!("/v3/team/{team_id}/members"), + format!("/v3/{team_association}/{team_association_id}/members"), + ] { + let req = test::TestRequest::get() + .uri(&uri) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let members = value.as_array().unwrap(); + assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team + let user_user = members + .iter() + .find(|x| x["user"]["id"] == USER_USER_ID) + .unwrap(); + let friend_user = members + .iter() + .find(|x| x["user"]["id"] == FRIEND_USER_ID) + .unwrap(); + assert_eq!(user_user["user"]["id"], USER_USER_ID); + assert!(user_user["permissions"].is_null()); // Should not see private data of the team + assert_eq!(friend_user["user"]["id"], FRIEND_USER_ID); + assert!(friend_user["permissions"].is_null()); + + let req = test::TestRequest::get() + .uri(&uri) + .append_header(("Authorization", ENEMY_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let members = value.as_array().unwrap(); + assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team + assert_eq!(members[0]["user"]["id"], USER_USER_ID); + assert!(members[0]["permissions"].is_null()); + } + // An accepted member of the team should appear in the team members list + // and should be able to see private data about the team + let req = test::TestRequest::post() + .uri(&format!("/v3/team/{team_id}/join")) .append_header(("Authorization", FRIEND_USER_PAT)) .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - let value: serde_json::Value = test::read_body_json(resp).await; - assert_eq!(value[0]["user"]["id"], USER_USER_ID); - assert!(value[0]["permissions"].is_null()); + assert_eq!(resp.status(), 204); + + for uri in [ + format!("/v3/team/{team_id}/members"), + format!("/v3/{team_association}/{team_association_id}/members"), + ] { + let req = test::TestRequest::get() + .uri(&uri) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let members = value.as_array().unwrap(); + assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team + let user_user = members + .iter() + .find(|x| x["user"]["id"] == USER_USER_ID) + .unwrap(); + let friend_user = members + .iter() + .find(|x| x["user"]["id"] == FRIEND_USER_ID) + .unwrap(); + assert_eq!(user_user["user"]["id"], USER_USER_ID); + assert!(!user_user["permissions"].is_null()); // SHOULD see private data of the team + assert_eq!(friend_user["user"]["id"], FRIEND_USER_ID); + assert!(!friend_user["permissions"].is_null()); + } } + }) + .await; +} - // A non-accepted member of the team should: - // - not be able to see private data about the team, but see all members including themselves - // - should not appear in the team members list to enemy users +#[actix_rt::test] +async fn test_get_team_project_orgs() { + // Test setup and dummy data + with_test_environment_all(None, |test_env| async move { + let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + // Attach alpha to zeta let req = test::TestRequest::post() - .uri(&format!("/v3/team/{team_id}/members")) + .uri(&format!("/v3/organization/{zeta_organization_id}/projects")) .append_header(("Authorization", USER_USER_PAT)) - .set_json(&json!({ + .set_json(json!({ + "project_id": alpha_project_id, + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + + // Invite and add friend to zeta + let req = test::TestRequest::post() + .uri(&format!("/v3/team/{zeta_team_id}/members")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ "user_id": FRIEND_USER_ID, })) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 204); - for uri in [ - format!("/v3/team/{team_id}/members"), - format!("/v3/{team_association}/{team_association_id}/members"), - ] { - let req = test::TestRequest::get() - .uri(&uri) - .append_header(("Authorization", FRIEND_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - let value: serde_json::Value = test::read_body_json(resp).await; - let members = value.as_array().unwrap(); - assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team - let user_user = members - .iter() - .find(|x| x["user"]["id"] == USER_USER_ID) - .unwrap(); - let friend_user = members - .iter() - .find(|x| x["user"]["id"] == FRIEND_USER_ID) - .unwrap(); - assert_eq!(user_user["user"]["id"], USER_USER_ID); - assert!(user_user["permissions"].is_null()); // Should not see private data of the team - assert_eq!(friend_user["user"]["id"], FRIEND_USER_ID); - assert!(friend_user["permissions"].is_null()); - - let req = test::TestRequest::get() - .uri(&uri) - .append_header(("Authorization", ENEMY_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - let value: serde_json::Value = test::read_body_json(resp).await; - let members = value.as_array().unwrap(); - assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team - assert_eq!(members[0]["user"]["id"], USER_USER_ID); - assert!(members[0]["permissions"].is_null()); - } - // An accepted member of the team should appear in the team members list - // and should be able to see private data about the team let req = test::TestRequest::post() - .uri(&format!("/v3/team/{team_id}/join")) + .uri(&format!("/v3/team/{zeta_team_id}/join")) .append_header(("Authorization", FRIEND_USER_PAT)) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 204); - for uri in [ - format!("/v3/team/{team_id}/members"), - format!("/v3/{team_association}/{team_association_id}/members"), - ] { - let req = test::TestRequest::get() - .uri(&uri) - .append_header(("Authorization", FRIEND_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - let value: serde_json::Value = test::read_body_json(resp).await; - let members = value.as_array().unwrap(); - assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team - let user_user = members - .iter() - .find(|x| x["user"]["id"] == USER_USER_ID) - .unwrap(); - let friend_user = members - .iter() - .find(|x| x["user"]["id"] == FRIEND_USER_ID) - .unwrap(); - assert_eq!(user_user["user"]["id"], USER_USER_ID); - assert!(!user_user["permissions"].is_null()); // SHOULD see private data of the team - assert_eq!(friend_user["user"]["id"], FRIEND_USER_ID); - assert!(!friend_user["permissions"].is_null()); - } - } - - // Cleanup test db - test_env.cleanup().await; -} - -#[actix_rt::test] -async fn test_get_team_project_orgs() { - // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; - let zeta_organization_id = &test_env - .dummy - .as_ref() - .unwrap() - .organization_zeta - .organization_id; - let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; - - // Attach alpha to zeta - let req = test::TestRequest::post() - .uri(&format!("/v3/organization/{zeta_organization_id}/projects")) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "project_id": alpha_project_id, - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - - // Invite and add friend to zeta - let req = test::TestRequest::post() - .uri(&format!("/v3/team/{zeta_team_id}/members")) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "user_id": FRIEND_USER_ID, - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - let req = test::TestRequest::post() - .uri(&format!("/v3/team/{zeta_team_id}/join")) - .append_header(("Authorization", FRIEND_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // The team members route from teams (on a project's team): - // - the members of the project team specifically - // - not the ones from the organization - let req = test::TestRequest::get() - .uri(&format!("/v3/team/{alpha_team_id}/members")) - .append_header(("Authorization", FRIEND_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - let value: serde_json::Value = test::read_body_json(resp).await; - let members = value.as_array().unwrap(); - assert_eq!(members.len(), 1); - - // The team members route from project should show: - // - the members of the project team including the ones from the organization - let req = test::TestRequest::get() - .uri(&format!("/v3/project/{alpha_project_id}/members")) - .append_header(("Authorization", FRIEND_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - let value: serde_json::Value = test::read_body_json(resp).await; - let members = value.as_array().unwrap(); - assert_eq!(members.len(), 2); - - // Cleanup test db - test_env.cleanup().await; + // The team members route from teams (on a project's team): + // - the members of the project team specifically + // - not the ones from the organization + let req = test::TestRequest::get() + .uri(&format!("/v3/team/{alpha_team_id}/members")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let members = value.as_array().unwrap(); + assert_eq!(members.len(), 1); + + // The team members route from project should show: + // - the members of the project team including the ones from the organization + let req = test::TestRequest::get() + .uri(&format!("/v3/project/{alpha_project_id}/members")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let members = value.as_array().unwrap(); + assert_eq!(members.len(), 2); + }) + .await; } // edit team member (Varying permissions, varying roles) #[actix_rt::test] async fn test_patch_project_team_member() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; - - // Edit team as admin/mod but not a part of the team should be OK - let req = test::TestRequest::patch() - .uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) - .set_json(json!({})) - .append_header(("Authorization", ADMIN_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // As a non-owner with full permissions, attempt to edit the owner's permissions/roles - let req = test::TestRequest::patch() - .uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) - .append_header(("Authorization", ADMIN_USER_PAT)) - .set_json(json!({ - "role": "member" - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 400); - - let req = test::TestRequest::patch() - .uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) - .append_header(("Authorization", ADMIN_USER_PAT)) - .set_json(json!({ - "permissions": 0 - })) - .to_request(); - let resp = test_env.call(req).await; - - assert_eq!(resp.status(), 400); - - // Should not be able to edit organization permissions of a project team - let req = test::TestRequest::patch() - .uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "organization_permissions": 0 - })) - .to_request(); - let resp = test_env.call(req).await; - - assert_eq!(resp.status(), 400); - - // Should not be able to add permissions to a user that the adding-user does not have - // (true for both project and org) - - // first, invite friend - let req = test::TestRequest::post() - .uri(&format!("/v3/team/{alpha_team_id}/members")) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "user_id": FRIEND_USER_ID, - "permissions": (ProjectPermissions::EDIT_MEMBER | ProjectPermissions::EDIT_BODY).bits(), - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // accept - let req = test::TestRequest::post() - .uri(&format!("/v3/team/{alpha_team_id}/join")) + with_test_environment_all(None, |test_env| async move { + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + + // Edit team as admin/mod but not a part of the team should be OK + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) + .set_json(json!({})) + .append_header(("Authorization", ADMIN_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // As a non-owner with full permissions, attempt to edit the owner's permissions/roles + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) + .append_header(("Authorization", ADMIN_USER_PAT)) + .set_json(json!({ + "role": "member" + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) + .append_header(("Authorization", ADMIN_USER_PAT)) + .set_json(json!({ + "permissions": 0 + })) + .to_request(); + let resp = test_env.call(req).await; + + assert_eq!(resp.status(), 400); + + // Should not be able to edit organization permissions of a project team + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{alpha_team_id}/members/{USER_USER_ID}")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "organization_permissions": 0 + })) + .to_request(); + let resp = test_env.call(req).await; + + assert_eq!(resp.status(), 400); + + // Should not be able to add permissions to a user that the adding-user does not have + // (true for both project and org) + + // first, invite friend + let req = test::TestRequest::post() + .uri(&format!("/v3/team/{alpha_team_id}/members")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "user_id": FRIEND_USER_ID, + "permissions": (ProjectPermissions::EDIT_MEMBER | ProjectPermissions::EDIT_BODY).bits(), + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // accept + let req = test::TestRequest::post() + .uri(&format!("/v3/team/{alpha_team_id}/join")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // try to add permissions + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}")) .append_header(("Authorization", FRIEND_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // try to add permissions - let req = test::TestRequest::patch() - .uri(&format!("/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}")) - .append_header(("Authorization", FRIEND_USER_PAT)) - .set_json(json!({ - "permissions": (ProjectPermissions::EDIT_MEMBER | ProjectPermissions::EDIT_DETAILS).bits() - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 400); - - // Cannot set a user to Owner - let req = test::TestRequest::patch() - .uri(&format!( - "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" - )) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "role": "Owner" - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 400); - - // Cannot set payouts outside of 0 and 5000 - for payout in [-1, 5001] { + .set_json(json!({ + "permissions": (ProjectPermissions::EDIT_MEMBER | ProjectPermissions::EDIT_DETAILS).bits() + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + + // Cannot set a user to Owner let req = test::TestRequest::patch() .uri(&format!( "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" )) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ - "payouts_split": payout + "role": "Owner" })) .to_request(); let resp = test_env.call(req).await; - assert_eq!(resp.status(), 400); - } - // Successful patch - let req = test::TestRequest::patch() - .uri(&format!( - "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" - )) - .append_header(("Authorization", FRIEND_USER_PAT)) - .set_json(json!({ - "payouts_split": 51, - "permissions": ProjectPermissions::EDIT_MEMBER.bits(), // reduces permissions - "role": "member", - "ordering": 5 - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // Check results - let req = test::TestRequest::get() - .uri(&format!("/v3/team/{alpha_team_id}/members")) - .append_header(("Authorization", FRIEND_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - let value: serde_json::Value = test::read_body_json(resp).await; - let member = value - .as_array() - .unwrap() - .iter() - .find(|x| x["user"]["id"] == FRIEND_USER_ID) - .unwrap(); - assert_eq!(member["payouts_split"], 51.0); - assert_eq!( - member["permissions"], - ProjectPermissions::EDIT_MEMBER.bits() - ); - assert_eq!(member["role"], "member"); - assert_eq!(member["ordering"], 5); - - // Cleanup test db - test_env.cleanup().await; + // Cannot set payouts outside of 0 and 5000 + for payout in [-1, 5001] { + let req = test::TestRequest::patch() + .uri(&format!( + "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + )) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "payouts_split": payout + })) + .to_request(); + let resp = test_env.call(req).await; + + assert_eq!(resp.status(), 400); + } + + // Successful patch + let req = test::TestRequest::patch() + .uri(&format!( + "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + )) + .append_header(("Authorization", FRIEND_USER_PAT)) + .set_json(json!({ + "payouts_split": 51, + "permissions": ProjectPermissions::EDIT_MEMBER.bits(), // reduces permissions + "role": "member", + "ordering": 5 + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // Check results + let req = test::TestRequest::get() + .uri(&format!("/v3/team/{alpha_team_id}/members")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let member = value + .as_array() + .unwrap() + .iter() + .find(|x| x["user"]["id"] == FRIEND_USER_ID) + .unwrap(); + assert_eq!(member["payouts_split"], 51.0); + assert_eq!( + member["permissions"], + ProjectPermissions::EDIT_MEMBER.bits() + ); + assert_eq!(member["role"], "member"); + assert_eq!(member["ordering"], 5); + + }).await; } // edit team member (Varying permissions, varying roles) #[actix_rt::test] async fn test_patch_organization_team_member() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; - - // Edit team as admin/mod but not a part of the team should be OK - let req = test::TestRequest::patch() - .uri(&format!("/v3/team/{zeta_team_id}/members/{USER_USER_ID}")) - .set_json(json!({})) - .append_header(("Authorization", ADMIN_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // As a non-owner with full permissions, attempt to edit the owner's permissions/roles - let req = test::TestRequest::patch() - .uri(&format!("/v3/team/{zeta_team_id}/members/{USER_USER_ID}")) - .append_header(("Authorization", ADMIN_USER_PAT)) - .set_json(json!({ - "role": "member" - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 400); - - let req = test::TestRequest::patch() - .uri(&format!("/v3/team/{zeta_team_id}/members/{USER_USER_ID}")) - .append_header(("Authorization", ADMIN_USER_PAT)) - .set_json(json!({ - "permissions": 0 - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 400); - - // Should not be able to add permissions to a user that the adding-user does not have - // (true for both project and org) - - // first, invite friend - let req = test::TestRequest::post() - .uri(&format!("/v3/team/{zeta_team_id}/members")) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "user_id": FRIEND_USER_ID, - "organization_permissions": (OrganizationPermissions::EDIT_MEMBER | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS).bits(), - })).to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // accept - let req = test::TestRequest::post() - .uri(&format!("/v3/team/{zeta_team_id}/join")) - .append_header(("Authorization", FRIEND_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // try to add permissions- fails, as we do not have EDIT_DETAILS - let req = test::TestRequest::patch() - .uri(&format!("/v3/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) - .append_header(("Authorization", FRIEND_USER_PAT)) - .set_json(json!({ - "organization_permissions": (OrganizationPermissions::EDIT_MEMBER | OrganizationPermissions::EDIT_DETAILS).bits() - })) - .to_request(); - let resp = test_env.call(req).await; - - assert_eq!(resp.status(), 400); - - // Cannot set a user to Owner - let req = test::TestRequest::patch() + with_test_environment_all(None, |test_env| async move { + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + // Edit team as admin/mod but not a part of the team should be OK + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{zeta_team_id}/members/{USER_USER_ID}")) + .set_json(json!({})) + .append_header(("Authorization", ADMIN_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // As a non-owner with full permissions, attempt to edit the owner's permissions/roles + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{zeta_team_id}/members/{USER_USER_ID}")) + .append_header(("Authorization", ADMIN_USER_PAT)) + .set_json(json!({ + "role": "member" + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{zeta_team_id}/members/{USER_USER_ID}")) + .append_header(("Authorization", ADMIN_USER_PAT)) + .set_json(json!({ + "permissions": 0 + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + + // Should not be able to add permissions to a user that the adding-user does not have + // (true for both project and org) + + // first, invite friend + let req = test::TestRequest::post() + .uri(&format!("/v3/team/{zeta_team_id}/members")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "user_id": FRIEND_USER_ID, + "organization_permissions": (OrganizationPermissions::EDIT_MEMBER | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS).bits(), + })).to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // accept + let req = test::TestRequest::post() + .uri(&format!("/v3/team/{zeta_team_id}/join")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // try to add permissions- fails, as we do not have EDIT_DETAILS + let req = test::TestRequest::patch() .uri(&format!("/v3/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "role": "Owner" - })) - .to_request(); - let resp = test_env.call(req).await; + .append_header(("Authorization", FRIEND_USER_PAT)) + .set_json(json!({ + "organization_permissions": (OrganizationPermissions::EDIT_MEMBER | OrganizationPermissions::EDIT_DETAILS).bits() + })) + .to_request(); + let resp = test_env.call(req).await; - assert_eq!(resp.status(), 400); + assert_eq!(resp.status(), 400); - // Cannot set payouts outside of 0 and 5000 - for payout in [-1, 5001] { + // Cannot set a user to Owner let req = test::TestRequest::patch() .uri(&format!("/v3/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) .append_header(("Authorization", USER_USER_PAT)) .set_json(json!({ - "payouts_split": payout + "role": "Owner" })) .to_request(); let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); - } - // Successful patch - let req = test::TestRequest::patch() - .uri(&format!("/v3/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) - .append_header(("Authorization", FRIEND_USER_PAT)) - .set_json(json!({ - "payouts_split": 51, - "organization_permissions": (OrganizationPermissions::EDIT_MEMBER).bits(), // reduces permissions - "permissions": (ProjectPermissions::EDIT_MEMBER).bits(), - "role": "member", - "ordering": 5 - })) - .to_request(); - let resp = test_env.call(req).await; - - assert_eq!(resp.status(), 204); - - // Check results - let req = test::TestRequest::get() - .uri(&format!("/v3/team/{zeta_team_id}/members")) - .append_header(("Authorization", FRIEND_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - let value: serde_json::Value = test::read_body_json(resp).await; - let member = value - .as_array() - .unwrap() - .iter() - .find(|x| x["user"]["id"] == FRIEND_USER_ID) - .unwrap(); - assert_eq!(member["payouts_split"], 51.0); - assert_eq!( - member["organization_permissions"], - OrganizationPermissions::EDIT_MEMBER.bits() - ); - assert_eq!( - member["permissions"], - ProjectPermissions::EDIT_MEMBER.bits() - ); - assert_eq!(member["role"], "member"); - assert_eq!(member["ordering"], 5); - - // Cleanup test db - test_env.cleanup().await; + // Cannot set payouts outside of 0 and 5000 + for payout in [-1, 5001] { + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "payouts_split": payout + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + } + + // Successful patch + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .set_json(json!({ + "payouts_split": 51, + "organization_permissions": (OrganizationPermissions::EDIT_MEMBER).bits(), // reduces permissions + "permissions": (ProjectPermissions::EDIT_MEMBER).bits(), + "role": "member", + "ordering": 5 + })) + .to_request(); + let resp = test_env.call(req).await; + + assert_eq!(resp.status(), 204); + + // Check results + let req = test::TestRequest::get() + .uri(&format!("/v3/team/{zeta_team_id}/members")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let member = value + .as_array() + .unwrap() + .iter() + .find(|x| x["user"]["id"] == FRIEND_USER_ID) + .unwrap(); + assert_eq!(member["payouts_split"], 51.0); + assert_eq!( + member["organization_permissions"], + OrganizationPermissions::EDIT_MEMBER.bits() + ); + assert_eq!( + member["permissions"], + ProjectPermissions::EDIT_MEMBER.bits() + ); + assert_eq!(member["role"], "member"); + assert_eq!(member["ordering"], 5); + + }).await; } // trasnfer ownership (requires being owner, etc) #[actix_rt::test] async fn transfer_ownership() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; - - // Cannot set friend as owner (not a member) - let req = test::TestRequest::patch() - .uri(&format!("/v3/team/{alpha_team_id}/owner")) - .set_json(json!({ - "user_id": FRIEND_USER_ID - })) - .append_header(("Authorization", USER_USER_ID)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 401); - - // first, invite friend - let req = test::TestRequest::post() - .uri(&format!("/v3/team/{alpha_team_id}/members")) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "user_id": FRIEND_USER_ID, - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // accept - let req = test::TestRequest::post() - .uri(&format!("/v3/team/{alpha_team_id}/join")) - .append_header(("Authorization", FRIEND_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // Cannot set ourselves as owner - let req = test::TestRequest::patch() - .uri(&format!("/v3/team/{alpha_team_id}/owner")) - .set_json(json!({ - "user_id": FRIEND_USER_ID - })) - .append_header(("Authorization", FRIEND_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 401); - - // Can set friend as owner - let req = test::TestRequest::patch() - .uri(&format!("/v3/team/{alpha_team_id}/owner")) - .set_json(json!({ - "user_id": FRIEND_USER_ID - })) - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 204); - - // Check - let req = test::TestRequest::get() - .uri(&format!("/v3/team/{alpha_team_id}/members")) - .set_json(json!({ - "user_id": FRIEND_USER_ID - })) - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - let value: serde_json::Value = test::read_body_json(resp).await; - let friend_member = value - .as_array() - .unwrap() - .iter() - .find(|x| x["user"]["id"] == FRIEND_USER_ID) - .unwrap(); - assert_eq!(friend_member["role"], "Owner"); - assert_eq!( - friend_member["permissions"], - ProjectPermissions::all().bits() - ); - let user_member = value - .as_array() - .unwrap() - .iter() - .find(|x| x["user"]["id"] == USER_USER_ID) - .unwrap(); - assert_eq!(user_member["role"], "Member"); - assert_eq!(user_member["permissions"], ProjectPermissions::all().bits()); - - // Confirm that user, a user who still has full permissions, cannot then remove the owner - let req = test::TestRequest::delete() - .uri(&format!( - "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" - )) - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 401); - - // Cleanup test db - test_env.cleanup().await; + with_test_environment_all(None, |test_env| async move { + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + + // Cannot set friend as owner (not a member) + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{alpha_team_id}/owner")) + .set_json(json!({ + "user_id": FRIEND_USER_ID + })) + .append_header(("Authorization", USER_USER_ID)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 401); + + // first, invite friend + let req = test::TestRequest::post() + .uri(&format!("/v3/team/{alpha_team_id}/members")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "user_id": FRIEND_USER_ID, + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // accept + let req = test::TestRequest::post() + .uri(&format!("/v3/team/{alpha_team_id}/join")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // Cannot set ourselves as owner + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{alpha_team_id}/owner")) + .set_json(json!({ + "user_id": FRIEND_USER_ID + })) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 401); + + // Can set friend as owner + let req = test::TestRequest::patch() + .uri(&format!("/v3/team/{alpha_team_id}/owner")) + .set_json(json!({ + "user_id": FRIEND_USER_ID + })) + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // Check + let req = test::TestRequest::get() + .uri(&format!("/v3/team/{alpha_team_id}/members")) + .set_json(json!({ + "user_id": FRIEND_USER_ID + })) + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let friend_member = value + .as_array() + .unwrap() + .iter() + .find(|x| x["user"]["id"] == FRIEND_USER_ID) + .unwrap(); + assert_eq!(friend_member["role"], "Owner"); + assert_eq!( + friend_member["permissions"], + ProjectPermissions::all().bits() + ); + let user_member = value + .as_array() + .unwrap() + .iter() + .find(|x| x["user"]["id"] == USER_USER_ID) + .unwrap(); + assert_eq!(user_member["role"], "Member"); + assert_eq!(user_member["permissions"], ProjectPermissions::all().bits()); + + // Confirm that user, a user who still has full permissions, cannot then remove the owner + let req = test::TestRequest::delete() + .uri(&format!( + "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + )) + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 401); + }) + .await; } // This test is currently not working. @@ -617,7 +612,7 @@ async fn transfer_ownership() { // // This is because project-team permission overrriding must be possible, and this overriding can decrease the number of permissions a user has. // let test_env = TestEnvironment::build(None).await; -// let api = &test_env.v3; +// let api = &test_env.api; // let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; // let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; diff --git a/tests/user.rs b/tests/user.rs index 75a97b83..8426829e 100644 --- a/tests/user.rs +++ b/tests/user.rs @@ -1,11 +1,10 @@ +use crate::common::api_common::{ApiProject, ApiTeams}; +use common::dummy_data::TestFile; use common::{ database::{FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_ID, USER_USER_PAT}, - environment::with_test_environment, + environment::with_test_environment_all, }; -use crate::common::api_v3::request_data::get_public_project_creation_data; -use common::dummy_data::TestFile; - mod common; // user GET (permissions, different users) @@ -19,20 +18,17 @@ mod common; #[actix_rt::test] pub async fn get_user_projects_after_creating_project_returns_new_project() { - with_test_environment(|test_env| async move { - let api = test_env.v3; - api.get_user_projects_deserialized(USER_USER_ID, USER_USER_PAT) + with_test_environment_all(None, |test_env| async move { + let api = test_env.api; + api.get_user_projects_deserialized_common(USER_USER_ID, USER_USER_PAT) .await; let (project, _) = api - .add_public_project( - get_public_project_creation_data("slug", Some(TestFile::BasicMod)), - USER_USER_PAT, - ) + .add_public_project("slug", Some(TestFile::BasicMod), None, USER_USER_PAT) .await; let resp_projects = api - .get_user_projects_deserialized(USER_USER_ID, USER_USER_PAT) + .get_user_projects_deserialized_common(USER_USER_ID, USER_USER_PAT) .await; assert!(resp_projects.iter().any(|p| p.id == project.id)); }) @@ -41,22 +37,19 @@ pub async fn get_user_projects_after_creating_project_returns_new_project() { #[actix_rt::test] pub async fn get_user_projects_after_deleting_project_shows_removal() { - with_test_environment(|test_env| async move { - let api = test_env.v3; + with_test_environment_all(None, |test_env| async move { + let api = test_env.api; let (project, _) = api - .add_public_project( - get_public_project_creation_data("iota", Some(TestFile::BasicMod)), - USER_USER_PAT, - ) + .add_public_project("iota", Some(TestFile::BasicMod), None, USER_USER_PAT) .await; - api.get_user_projects_deserialized(USER_USER_ID, USER_USER_PAT) + api.get_user_projects_deserialized_common(USER_USER_ID, USER_USER_PAT) .await; api.remove_project(project.slug.as_ref().unwrap(), USER_USER_PAT) .await; let resp_projects = api - .get_user_projects_deserialized(USER_USER_ID, USER_USER_PAT) + .get_user_projects_deserialized_common(USER_USER_ID, USER_USER_PAT) .await; assert!(!resp_projects.iter().any(|p| p.id == project.id)); }) @@ -65,11 +58,11 @@ pub async fn get_user_projects_after_deleting_project_shows_removal() { #[actix_rt::test] pub async fn get_user_projects_after_joining_team_shows_team_projects() { - with_test_environment(|test_env| async move { + with_test_environment_all(None, |test_env| async move { let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let api = test_env.v3; - api.get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) + let api = test_env.api; + api.get_user_projects_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) .await; api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) @@ -77,7 +70,7 @@ pub async fn get_user_projects_after_joining_team_shows_team_projects() { api.join_team(alpha_team_id, FRIEND_USER_PAT).await; let projects = api - .get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_projects_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) .await; assert!(projects .iter() @@ -88,21 +81,21 @@ pub async fn get_user_projects_after_joining_team_shows_team_projects() { #[actix_rt::test] pub async fn get_user_projects_after_leaving_team_shows_no_team_projects() { - with_test_environment(|test_env| async move { + with_test_environment_all(None, |test_env| async move { let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let api = test_env.v3; + let api = test_env.api; api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) .await; api.join_team(alpha_team_id, FRIEND_USER_PAT).await; - api.get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) + api.get_user_projects_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) .await; api.remove_from_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) .await; let projects = api - .get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) + .get_user_projects_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT) .await; assert!(!projects .iter() diff --git a/tests/v2/project.rs b/tests/v2/project.rs index 7e56d3a6..2bb4c757 100644 --- a/tests/v2/project.rs +++ b/tests/v2/project.rs @@ -1,8 +1,9 @@ use crate::common::{ - api_v2::request_data, + api_common::ApiProject, + api_v2::ApiV2, database::{ENEMY_USER_PAT, FRIEND_USER_ID, FRIEND_USER_PAT, MOD_USER_PAT, USER_USER_PAT}, dummy_data::{TestFile, DUMMY_CATEGORIES}, - environment::TestEnvironment, + environment::{with_test_environment, TestEnvironment}, permissions::{PermissionsTest, PermissionsTestContext}, }; use actix_web::test; @@ -16,522 +17,512 @@ use serde_json::json; #[actix_rt::test] async fn test_project_type_sanity() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; - - // Perform all other patch tests on both 'mod' and 'modpack' - let test_creation_mod = request_data::get_public_project_creation_data( - "test-mod", - Some(TestFile::build_random_jar()), - ); - let test_creation_modpack = request_data::get_public_project_creation_data( - "test-modpack", - Some(TestFile::build_random_mrpack()), - ); - for (mod_or_modpack, test_creation_data) in [ - ("mod", test_creation_mod), - ("modpack", test_creation_modpack), - ] { - let (test_project, test_version) = api - .add_public_project(test_creation_data, USER_USER_PAT) - .await; - let test_project_slug = test_project.slug.as_ref().unwrap(); + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + + // Perform all other patch tests on both 'mod' and 'modpack' + for (mod_or_modpack, slug, file) in [ + ("mod", "test-mod", TestFile::build_random_jar()), + ("modpack", "test-modpack", TestFile::build_random_mrpack()), + ] { + let (test_project, test_version) = api + .add_public_project(slug, Some(file), None, USER_USER_PAT) + .await; + let test_project_slug = test_project.slug.as_ref().unwrap(); + + // TODO: + // assert_eq!(test_project.project_type, mod_or_modpack); + assert_eq!(test_project.loaders, vec!["fabric"]); + assert_eq!(test_version[0].loaders, vec!["fabric"]); + + let project = api + .get_project_deserialized(test_project_slug, USER_USER_PAT) + .await; + assert_eq!(test_project.loaders, vec!["fabric"]); + assert_eq!(project.project_type, mod_or_modpack); + + let version = api + .get_version_deserialized(&test_version[0].id.to_string(), USER_USER_PAT) + .await; + assert_eq!( + version.loaders.iter().map(|x| &x.0).collect_vec(), + vec!["fabric"] + ); + } - assert_eq!(test_project.project_type, mod_or_modpack); - assert_eq!(test_project.loaders, vec!["fabric"]); - assert_eq!( - test_version[0].loaders.iter().map(|x| &x.0).collect_vec(), - vec!["fabric"] - ); + // TODO: as we get more complicated strucures with v3 testing, and alpha/beta get more complicated, we should add more tests here, + // to ensure that projects created with v3 routes are still valid and work with v3 routes. + }) + .await; +} - let project = api - .get_project_deserialized(test_project_slug, USER_USER_PAT) - .await; - assert_eq!(test_project.loaders, vec!["fabric"]); - assert_eq!(project.project_type, mod_or_modpack); +#[actix_rt::test] +async fn test_add_remove_project() { + // Test setup and dummy data + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + + // Generate test project data. + let mut json_data = json!( + { + "title": "Test_Add_Project project", + "slug": "demo", + "description": "Example description.", + "body": "Example body.", + "client_side": "required", + "server_side": "optional", + "initial_versions": [{ + "file_parts": ["basic-mod.jar"], + "version_number": "1.2.3", + "version_title": "start", + "dependencies": [], + "game_versions": ["1.20.1"] , + "release_channel": "release", + "loaders": ["fabric"], + "featured": true + }], + "categories": [], + "license_id": "MIT" + } + ); + // Basic json + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + }; + + // Basic json, with a different file + json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); + let json_diff_file_segment = MultipartSegment { + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + ..json_segment.clone() + }; + + // Basic json, with a different file, and a different slug + json_data["slug"] = json!("new_demo"); + json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); + let json_diff_slug_file_segment = MultipartSegment { + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + ..json_segment.clone() + }; + + // Basic file + let file_segment = MultipartSegment { + name: "basic-mod.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + // TODO: look at these: can be simplified with TestFile + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod.jar").to_vec(), + ), + }; + + // Differently named file, with the same content (for hash testing) + let file_diff_name_segment = MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod-different.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod.jar").to_vec(), + ), + }; + + // Differently named file, with different content + let file_diff_name_content_segment = MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod-different.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod-different.jar").to_vec(), + ), + }; + + // Add a project- simple, should work. + let req = test::TestRequest::post() + .uri("/v2/project") + .append_header(("Authorization", USER_USER_PAT)) + .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + .to_request(); + let resp = test_env.call(req).await; + + let status = resp.status(); + assert_eq!(status, 200); + + // Get the project we just made, and confirm that it's correct + let project = api.get_project_deserialized("demo", USER_USER_PAT).await; + assert!(project.versions.len() == 1); + let uploaded_version_id = project.versions[0]; + + // Checks files to ensure they were uploaded and correctly identify the file + let hash = sha1::Sha1::from(include_bytes!("../../tests/files/basic-mod.jar")) + .digest() + .to_string(); let version = api - .get_version_deserialized(&test_version[0].id.to_string(), USER_USER_PAT) + .get_version_from_hash_deserialized(&hash, "sha1", USER_USER_PAT) .await; - assert_eq!( - version.loaders.iter().map(|x| &x.0).collect_vec(), - vec!["fabric"] - ); - } + assert_eq!(version.id, uploaded_version_id); + + // Reusing with a different slug and the same file should fail + // Even if that file is named differently + let req = test::TestRequest::post() + .uri("/v2/project") + .append_header(("Authorization", USER_USER_PAT)) + .set_multipart(vec![ + json_diff_slug_file_segment.clone(), // Different slug, different file name + file_diff_name_segment.clone(), // Different file name, same content + ]) + .to_request(); - // TODO: as we get more complicated strucures with v3 testing, and alpha/beta get more complicated, we should add more tests here, - // to ensure that projects created with v3 routes are still valid and work with v3 routes. -} + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); -#[actix_rt::test] -async fn test_add_remove_project() { - // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; - - // Generate test project data. - let mut json_data = json!( - { - "title": "Test_Add_Project project", - "slug": "demo", - "description": "Example description.", - "body": "Example body.", - "client_side": "required", - "server_side": "optional", - "initial_versions": [{ - "file_parts": ["basic-mod.jar"], - "version_number": "1.2.3", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "release_channel": "release", - "loaders": ["fabric"], - "featured": true - }], - "categories": [], - "license_id": "MIT" - } - ); - - // Basic json - let json_segment = MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - }; - - // Basic json, with a different file - json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); - let json_diff_file_segment = MultipartSegment { - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - ..json_segment.clone() - }; - - // Basic json, with a different file, and a different slug - json_data["slug"] = json!("new_demo"); - json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar"); - let json_diff_slug_file_segment = MultipartSegment { - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - ..json_segment.clone() - }; - - // Basic file - let file_segment = MultipartSegment { - name: "basic-mod.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - // TODO: look at these: can be simplified with TestFile - data: MultipartSegmentData::Binary( - include_bytes!("../../tests/files/basic-mod.jar").to_vec(), - ), - }; - - // Differently named file, with the same content (for hash testing) - let file_diff_name_segment = MultipartSegment { - name: "basic-mod-different.jar".to_string(), - filename: Some("basic-mod-different.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../../tests/files/basic-mod.jar").to_vec(), - ), - }; - - // Differently named file, with different content - let file_diff_name_content_segment = MultipartSegment { - name: "basic-mod-different.jar".to_string(), - filename: Some("basic-mod-different.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../../tests/files/basic-mod-different.jar").to_vec(), - ), - }; - - // Add a project- simple, should work. - let req = test::TestRequest::post() - .uri("/v2/project") - .append_header(("Authorization", USER_USER_PAT)) - .set_multipart(vec![json_segment.clone(), file_segment.clone()]) - .to_request(); - let resp = test_env.call(req).await; - - let status = resp.status(); - assert_eq!(status, 200); - - // Get the project we just made, and confirm that it's correct - let project = api.get_project_deserialized("demo", USER_USER_PAT).await; - assert!(project.versions.len() == 1); - let uploaded_version_id = project.versions[0]; - - // Checks files to ensure they were uploaded and correctly identify the file - let hash = sha1::Sha1::from(include_bytes!("../../tests/files/basic-mod.jar")) - .digest() - .to_string(); - let version = api - .get_version_from_hash_deserialized(&hash, "sha1", USER_USER_PAT) - .await; - assert_eq!(version.id, uploaded_version_id); - - // Reusing with a different slug and the same file should fail - // Even if that file is named differently - let req = test::TestRequest::post() - .uri("/v2/project") - .append_header(("Authorization", USER_USER_PAT)) - .set_multipart(vec![ - json_diff_slug_file_segment.clone(), // Different slug, different file name - file_diff_name_segment.clone(), // Different file name, same content - ]) - .to_request(); - - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 400); - - // Reusing with the same slug and a different file should fail - let req = test::TestRequest::post() - .uri("/v2/project") - .append_header(("Authorization", USER_USER_PAT)) - .set_multipart(vec![ - json_diff_file_segment.clone(), // Same slug, different file name - file_diff_name_content_segment.clone(), // Different file name, different content - ]) - .to_request(); - - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 400); - - // Different slug, different file should succeed - let req = test::TestRequest::post() - .uri("/v2/project") - .append_header(("Authorization", USER_USER_PAT)) - .set_multipart(vec![ - json_diff_slug_file_segment.clone(), // Different slug, different file name - file_diff_name_content_segment.clone(), // Different file name, same content - ]) - .to_request(); - - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - - // Get - let project = api.get_project_deserialized("demo", USER_USER_PAT).await; - let id = project.id.to_string(); - - // Remove the project - let resp = test_env.v2.remove_project("demo", USER_USER_PAT).await; - assert_eq!(resp.status(), 204); - - // Confirm that the project is gone from the cache - let mut redis_conn = test_env.db.redis_pool.connect().await.unwrap(); - assert_eq!( - redis_conn - .get(PROJECTS_SLUGS_NAMESPACE, "demo") - .await - .unwrap() - .map(|x| x.parse::().unwrap()), - None - ); - assert_eq!( - redis_conn - .get(PROJECTS_SLUGS_NAMESPACE, &id) - .await - .unwrap() - .map(|x| x.parse::().unwrap()), - None - ); + // Reusing with the same slug and a different file should fail + let req = test::TestRequest::post() + .uri("/v2/project") + .append_header(("Authorization", USER_USER_PAT)) + .set_multipart(vec![ + json_diff_file_segment.clone(), // Same slug, different file name + file_diff_name_content_segment.clone(), // Different file name, different content + ]) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); - // Old slug no longer works - let resp = api.get_project("demo", USER_USER_PAT).await; - assert_eq!(resp.status(), 404); + // Different slug, different file should succeed + let req = test::TestRequest::post() + .uri("/v2/project") + .append_header(("Authorization", USER_USER_PAT)) + .set_multipart(vec![ + json_diff_slug_file_segment.clone(), // Different slug, different file name + file_diff_name_content_segment.clone(), // Different file name, same content + ]) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + + // Get + let project = api.get_project_deserialized("demo", USER_USER_PAT).await; + let id = project.id.to_string(); + + // Remove the project + let resp = test_env.api.remove_project("demo", USER_USER_PAT).await; + assert_eq!(resp.status(), 204); - // Cleanup test db - test_env.cleanup().await; + // Confirm that the project is gone from the cache + let mut redis_conn = test_env.db.redis_pool.connect().await.unwrap(); + assert_eq!( + redis_conn + .get(PROJECTS_SLUGS_NAMESPACE, "demo") + .await + .unwrap() + .map(|x| x.parse::().unwrap()), + None + ); + assert_eq!( + redis_conn + .get(PROJECTS_SLUGS_NAMESPACE, &id) + .await + .unwrap() + .map(|x| x.parse::().unwrap()), + None + ); + + // Old slug no longer works + let resp = api.get_project("demo", USER_USER_PAT).await; + assert_eq!(resp.status(), 404); + }) + .await; } #[actix_rt::test] async fn permissions_upload_version() { - let test_env = TestEnvironment::build(None).await; - let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; - let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; - let alpha_file_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash; - - let upload_version = ProjectPermissions::UPLOAD_VERSION; - - // Upload version with basic-mod.jar - let req_gen = |ctx: &PermissionsTestContext| { - test::TestRequest::post().uri("/v2/version").set_multipart([ - MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text( - serde_json::to_string(&json!({ - "project_id": ctx.project_id.unwrap(), - "file_parts": ["basic-mod.jar"], - "version_number": "1.0.0", - "version_title": "1.0.0", - "version_type": "release", - "dependencies": [], - "game_versions": ["1.20.1"], - "loaders": ["fabric"], - "featured": false, - - })) - .unwrap(), - ), - }, - MultipartSegment { - name: "basic-mod.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../../tests/files/basic-mod.jar").to_vec(), - ), - }, - ]) - }; - PermissionsTest::new(&test_env) - .simple_project_permissions_test(upload_version, req_gen) - .await - .unwrap(); - - // Upload file to existing version - // Uses alpha project, as it has an existing version - let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::post() - .uri(&format!("/v2/version/{}/file", alpha_version_id)) - .set_multipart([ + with_test_environment(None, |test_env: TestEnvironment| async move { + let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + let alpha_file_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash; + + let upload_version = ProjectPermissions::UPLOAD_VERSION; + + // Upload version with basic-mod.jar + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::post().uri("/v2/version").set_multipart([ MultipartSegment { name: "data".to_string(), filename: None, content_type: Some("application/json".to_string()), data: MultipartSegmentData::Text( serde_json::to_string(&json!({ - "file_parts": ["basic-mod-different.jar"], + "project_id": ctx.project_id.unwrap(), + "file_parts": ["basic-mod.jar"], + "version_number": "1.0.0", + "version_title": "1.0.0", + "version_type": "release", + "dependencies": [], + "game_versions": ["1.20.1"], + "loaders": ["fabric"], + "featured": false, + })) .unwrap(), ), }, MultipartSegment { - name: "basic-mod-different.jar".to_string(), - filename: Some("basic-mod-different.jar".to_string()), + name: "basic-mod.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), content_type: Some("application/java-archive".to_string()), data: MultipartSegmentData::Binary( - include_bytes!("../../tests/files/basic-mod-different.jar").to_vec(), + include_bytes!("../../tests/files/basic-mod.jar").to_vec(), ), }, ]) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(upload_version, req_gen) - .await - .unwrap(); - - // Patch version - // Uses alpha project, as it has an existing version - let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::patch() - .uri(&format!("/v2/version/{}", alpha_version_id)) - .set_json(json!({ - "name": "Basic Mod", - })) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(upload_version, req_gen) - .await - .unwrap(); - - // Delete version file - // Uses alpha project, as it has an existing version - let delete_version = ProjectPermissions::DELETE_VERSION; - let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!("/v2/version_file/{}", alpha_file_hash)) - }; - - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(delete_version, req_gen) - .await - .unwrap(); - - // Delete version - // Uses alpha project, as it has an existing version - let req_gen = |_: &PermissionsTestContext| { - test::TestRequest::delete().uri(&format!("/v2/version/{}", alpha_version_id)) - }; - PermissionsTest::new(&test_env) - .with_existing_project(alpha_project_id, alpha_team_id) - .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) - .simple_project_permissions_test(delete_version, req_gen) - .await - .unwrap(); - - test_env.cleanup().await; + }; + PermissionsTest::new(&test_env) + .simple_project_permissions_test(upload_version, req_gen) + .await + .unwrap(); + + // Upload file to existing version + // Uses alpha project, as it has an existing version + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::post() + .uri(&format!("/v2/version/{}/file", alpha_version_id)) + .set_multipart([ + MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text( + serde_json::to_string(&json!({ + "file_parts": ["basic-mod-different.jar"], + })) + .unwrap(), + ), + }, + MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod-different.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod-different.jar").to_vec(), + ), + }, + ]) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(upload_version, req_gen) + .await + .unwrap(); + + // Patch version + // Uses alpha project, as it has an existing version + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!("/v2/version/{}", alpha_version_id)) + .set_json(json!({ + "name": "Basic Mod", + })) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(upload_version, req_gen) + .await + .unwrap(); + + // Delete version file + // Uses alpha project, as it has an existing version + let delete_version = ProjectPermissions::DELETE_VERSION; + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!("/v2/version_file/{}", alpha_file_hash)) + }; + + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(delete_version, req_gen) + .await + .unwrap(); + + // Delete version + // Uses alpha project, as it has an existing version + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!("/v2/version/{}", alpha_version_id)) + }; + PermissionsTest::new(&test_env) + .with_existing_project(alpha_project_id, alpha_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_project_permissions_test(delete_version, req_gen) + .await + .unwrap(); + }) + .await; } #[actix_rt::test] pub async fn test_patch_project() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; - - let alpha_project_slug = &test_env.dummy.as_ref().unwrap().project_alpha.project_slug; - let beta_project_slug = &test_env.dummy.as_ref().unwrap().project_beta.project_slug; - - // First, we do some patch requests that should fail. - // Failure because the user is not authorized. - let resp = api - .edit_project( - alpha_project_slug, - json!({ - "title": "Test_Add_Project project - test 1", - }), - ENEMY_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 401); - - // Failure because we are setting URL fields to invalid urls. - for url_type in ["issues_url", "source_url", "wiki_url", "discord_url"] { - let resp = api - .edit_project( - alpha_project_slug, - json!({ - url_type: "w.fake.url", - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 400); - } + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; - // Failure because these are illegal requested statuses for a normal user. - for req in ["unknown", "processing", "withheld", "scheduled"] { + let alpha_project_slug = &test_env.dummy.as_ref().unwrap().project_alpha.project_slug; + let beta_project_slug = &test_env.dummy.as_ref().unwrap().project_beta.project_slug; + + // First, we do some patch requests that should fail. + // Failure because the user is not authorized. let resp = api .edit_project( alpha_project_slug, json!({ - "requested_status": req, + "title": "Test_Add_Project project - test 1", }), - USER_USER_PAT, + ENEMY_USER_PAT, ) .await; - assert_eq!(resp.status(), 400); - } + assert_eq!(resp.status(), 401); + + // Failure because we are setting URL fields to invalid urls. + for url_type in ["issues_url", "source_url", "wiki_url", "discord_url"] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + url_type: "w.fake.url", + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } - // Failure because these should not be able to be set by a non-mod - for key in ["moderation_message", "moderation_message_body"] { + // Failure because these are illegal requested statuses for a normal user. + for req in ["unknown", "processing", "withheld", "scheduled"] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + "requested_status": req, + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Failure because these should not be able to be set by a non-mod + for key in ["moderation_message", "moderation_message_body"] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + key: "test", + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 401); + + // (should work for a mod, though) + let resp = api + .edit_project( + alpha_project_slug, + json!({ + key: "test", + }), + MOD_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + } + + // Failed patch to alpha slug: + // - slug collision with beta + // - too short slug + // - too long slug + // - not url safe slug + // - not url safe slug + for slug in [ + beta_project_slug, + "a", + &"a".repeat(100), + "not url safe%&^!#$##!@#$%^&*()", + ] { + let resp = api + .edit_project( + alpha_project_slug, + json!({ + "slug": slug, // the other dummy project has this slug + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Not allowed to directly set status, as 'beta_project_slug' (the other project) is "processing" and cannot have its status changed like this. let resp = api .edit_project( - alpha_project_slug, + beta_project_slug, json!({ - key: "test", + "status": "private" }), USER_USER_PAT, ) .await; assert_eq!(resp.status(), 401); - // (should work for a mod, though) + // Sucessful request to patch many fields. let resp = api .edit_project( alpha_project_slug, json!({ - key: "test", - }), - MOD_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - } - - // Failed patch to alpha slug: - // - slug collision with beta - // - too short slug - // - too long slug - // - not url safe slug - // - not url safe slug - for slug in [ - beta_project_slug, - "a", - &"a".repeat(100), - "not url safe%&^!#$##!@#$%^&*()", - ] { - let resp = api - .edit_project( - alpha_project_slug, - json!({ - "slug": slug, // the other dummy project has this slug + "slug": "newslug", + "title": "New successful title", + "description": "New successful description", + "body": "New successful body", + "categories": [DUMMY_CATEGORIES[0]], + "license_id": "MIT", + "issues_url": "https://github.com", + "discord_url": "https://discord.gg", + "wiki_url": "https://wiki.com", + "client_side": "optional", + "server_side": "required", + "donation_urls": [{ + "id": "patreon", + "platform": "Patreon", + "url": "https://patreon.com" + }] }), USER_USER_PAT, ) .await; - assert_eq!(resp.status(), 400); - } + assert_eq!(resp.status(), 204); - // Not allowed to directly set status, as 'beta_project_slug' (the other project) is "processing" and cannot have its status changed like this. - let resp = api - .edit_project( - beta_project_slug, - json!({ - "status": "private" - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 401); - - // Sucessful request to patch many fields. - let resp = api - .edit_project( - alpha_project_slug, - json!({ - "slug": "newslug", - "title": "New successful title", - "description": "New successful description", - "body": "New successful body", - "categories": [DUMMY_CATEGORIES[0]], - "license_id": "MIT", - "issues_url": "https://github.com", - "discord_url": "https://discord.gg", - "wiki_url": "https://wiki.com", - "client_side": "optional", - "server_side": "required", - "donation_urls": [{ - "id": "patreon", - "platform": "Patreon", - "url": "https://patreon.com" - }] - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - - // Old slug no longer works - let resp = api.get_project(alpha_project_slug, USER_USER_PAT).await; - assert_eq!(resp.status(), 404); - - // New slug does work - let project = api.get_project_deserialized("newslug", USER_USER_PAT).await; - - assert_eq!(project.slug.unwrap(), "newslug"); - assert_eq!(project.title, "New successful title"); - assert_eq!(project.description, "New successful description"); - assert_eq!(project.body, "New successful body"); - assert_eq!(project.categories, vec![DUMMY_CATEGORIES[0]]); - assert_eq!(project.license.id, "MIT"); - assert_eq!(project.issues_url, Some("https://github.com".to_string())); - assert_eq!(project.discord_url, Some("https://discord.gg".to_string())); - assert_eq!(project.wiki_url, Some("https://wiki.com".to_string())); - assert_eq!(project.client_side.as_str(), "optional"); - assert_eq!(project.server_side.as_str(), "required"); - assert_eq!(project.donation_urls.unwrap()[0].url, "https://patreon.com"); - - // Cleanup test db - test_env.cleanup().await; + // Old slug no longer works + let resp = api.get_project(alpha_project_slug, USER_USER_PAT).await; + assert_eq!(resp.status(), 404); + + // New slug does work + let project = api.get_project_deserialized("newslug", USER_USER_PAT).await; + + assert_eq!(project.slug.unwrap(), "newslug"); + assert_eq!(project.title, "New successful title"); + assert_eq!(project.description, "New successful description"); + assert_eq!(project.body, "New successful body"); + assert_eq!(project.categories, vec![DUMMY_CATEGORIES[0]]); + assert_eq!(project.license.id, "MIT"); + assert_eq!(project.issues_url, Some("https://github.com".to_string())); + assert_eq!(project.discord_url, Some("https://discord.gg".to_string())); + assert_eq!(project.wiki_url, Some("https://wiki.com".to_string())); + assert_eq!(project.client_side.as_str(), "optional"); + assert_eq!(project.server_side.as_str(), "required"); + assert_eq!(project.donation_urls.unwrap()[0].url, "https://patreon.com"); + }) + .await; } diff --git a/tests/v2/scopes.rs b/tests/v2/scopes.rs index acb877f4..1648b90e 100644 --- a/tests/v2/scopes.rs +++ b/tests/v2/scopes.rs @@ -1,3 +1,5 @@ +use crate::common::api_v2::ApiV2; +use crate::common::environment::with_test_environment; use crate::common::environment::TestEnvironment; use crate::common::scopes::ScopeTest; use actix_web::test; @@ -10,100 +12,98 @@ use serde_json::json; // Project version creation scopes #[actix_rt::test] pub async fn project_version_create_scopes() { - let test_env = TestEnvironment::build(None).await; - - // Create project - let create_project = Scopes::PROJECT_CREATE; - let json_data = json!( - { - "title": "Test_Add_Project project", - "slug": "demo", - "description": "Example description.", - "body": "Example body.", - "initial_versions": [{ - "file_parts": ["basic-mod.jar"], - "version_number": "1.2.3", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "client_side": "required", - "server_side": "optional", - "release_channel": "release", - "loaders": ["fabric"], - "featured": true - }], - "categories": [], - "license_id": "MIT" - } - ); - let json_segment = MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - }; - let file_segment = MultipartSegment { - name: "basic-mod.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../../tests/files/basic-mod.jar").to_vec(), - ), - }; - - let req_gen = || { - test::TestRequest::post() - .uri("/v3/project") - .set_multipart(vec![json_segment.clone(), file_segment.clone()]) - }; - let (_, success) = ScopeTest::new(&test_env) - .test(req_gen, create_project) - .await - .unwrap(); - let project_id = success["id"].as_str().unwrap(); - - // Add version to project - let create_version = Scopes::VERSION_CREATE; - let json_data = json!( + with_test_environment(None, |test_env: TestEnvironment| async move { + // Create project + let create_project = Scopes::PROJECT_CREATE; + let json_data = json!( { - "project_id": project_id, - "file_parts": ["basic-mod-different.jar"], - "version_number": "1.2.3.4", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "client_side": "required", - "server_side": "optional", - "release_channel": "release", - "loaders": ["fabric"], - "featured": true + "title": "Test_Add_Project project", + "slug": "demo", + "description": "Example description.", + "body": "Example body.", + "initial_versions": [{ + "file_parts": ["basic-mod.jar"], + "version_number": "1.2.3", + "version_title": "start", + "dependencies": [], + "game_versions": ["1.20.1"] , + "client_side": "required", + "server_side": "optional", + "release_channel": "release", + "loaders": ["fabric"], + "featured": true + }], + "categories": [], + "license_id": "MIT" } - ); - let json_segment = MultipartSegment { - name: "data".to_string(), - filename: None, - content_type: Some("application/json".to_string()), - data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), - }; - let file_segment = MultipartSegment { - name: "basic-mod-different.jar".to_string(), - filename: Some("basic-mod.jar".to_string()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary( - include_bytes!("../../tests/files/basic-mod-different.jar").to_vec(), - ), - }; + ); + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + }; + let file_segment = MultipartSegment { + name: "basic-mod.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod.jar").to_vec(), + ), + }; + + let req_gen = || { + test::TestRequest::post() + .uri("/v3/project") + .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + }; + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, create_project) + .await + .unwrap(); + let project_id = success["id"].as_str().unwrap(); - let req_gen = || { - test::TestRequest::post() - .uri("/v3/version") - .set_multipart(vec![json_segment.clone(), file_segment.clone()]) - }; - ScopeTest::new(&test_env) - .test(req_gen, create_version) - .await - .unwrap(); + // Add version to project + let create_version = Scopes::VERSION_CREATE; + let json_data = json!( + { + "project_id": project_id, + "file_parts": ["basic-mod-different.jar"], + "version_number": "1.2.3.4", + "version_title": "start", + "dependencies": [], + "game_versions": ["1.20.1"] , + "client_side": "required", + "server_side": "optional", + "release_channel": "release", + "loaders": ["fabric"], + "featured": true + } + ); + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + }; + let file_segment = MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod-different.jar").to_vec(), + ), + }; - // Cleanup test db - test_env.cleanup().await; + let req_gen = || { + test::TestRequest::post() + .uri("/v3/version") + .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + }; + ScopeTest::new(&test_env) + .test(req_gen, create_version) + .await + .unwrap(); + }) + .await; } diff --git a/tests/v2/search.rs b/tests/v2/search.rs index 1e3ccbdf..4a365b05 100644 --- a/tests/v2/search.rs +++ b/tests/v2/search.rs @@ -1,9 +1,11 @@ -use crate::common::api_v2::request_data; -use crate::common::api_v2::request_data::get_public_version_creation_data; -use crate::common::api_v2::request_data::ProjectCreationRequestData; +use crate::common::api_common::Api; +use crate::common::api_common::ApiProject; +use crate::common::api_common::ApiVersion; +use crate::common::api_v2::ApiV2; use crate::common::database::*; use crate::common::dummy_data::TestFile; use crate::common::dummy_data::DUMMY_CATEGORIES; +use crate::common::environment::with_test_environment; use crate::common::environment::TestEnvironment; use futures::stream::StreamExt; use labrinth::models::ids::base62_impl::parse_base62; @@ -17,283 +19,285 @@ async fn search_projects() { // It should drastically simplify this function // Test setup and dummy data - let test_env = TestEnvironment::build(Some(10)).await; - let api = &test_env.v2; - let test_name = test_env.db.database_name.clone(); + with_test_environment(Some(10), |test_env: TestEnvironment| async move { + let api = &test_env.api; + let test_name = test_env.db.database_name.clone(); - // Add dummy projects of various categories for searchability - let mut project_creation_futures = vec![]; + // Add dummy projects of various categories for searchability + let mut project_creation_futures = vec![]; - let create_async_future = - |id: u64, - pat: &'static str, - is_modpack: bool, - modify_json: Box| { - let slug = format!("{test_name}-searchable-project-{id}"); + let create_async_future = + |id: u64, + pat: &'static str, + is_modpack: bool, + modify_json: Option| { + let slug = format!("{test_name}-searchable-project-{id}"); - let jar = if is_modpack { - TestFile::build_random_mrpack() - } else { - TestFile::build_random_jar() - }; - let mut basic_project_json = - request_data::get_public_project_creation_data_json(&slug, Some(&jar)); - modify_json(&mut basic_project_json); - - let basic_project_multipart = - request_data::get_public_creation_data_multipart(&basic_project_json, Some(&jar)); - // Add a project- simple, should work. - let req = api.add_public_project( - ProjectCreationRequestData { - slug, - jar: Some(jar), - segment_data: basic_project_multipart, - }, - pat, - ); - async move { - let (project, _) = req.await; + let jar = if is_modpack { + TestFile::build_random_mrpack() + } else { + TestFile::build_random_jar() + }; + async move { + // Add a project- simple, should work. + let req = api.add_public_project(&slug, Some(jar), modify_json, pat); + let (project, _) = req.await; - // Approve, so that the project is searchable - let resp = api - .edit_project( - &project.id.to_string(), - json!({ - "status": "approved" - }), - MOD_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - (project.id.0, id) - } - }; + // Approve, so that the project is searchable + let resp = api + .edit_project( + &project.id.to_string(), + json!({ + "status": "approved" + }), + MOD_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + (project.id.0, id) + } + }; - // Test project 0 - let id = 0; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[4..6]); - json["server_side"] = json!("required"); - json["license_id"] = json!("LGPL-3.0-or-later"); - }; - project_creation_futures.push(create_async_future( - id, - USER_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 0 + let id = 0; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[4..6] }, + { "op": "add", "path": "/server_side", "value": "required" }, + { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Some(modify_json), + )); - // Test project 1 - let id = 1; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[0..2]); - json["client_side"] = json!("optional"); - }; - project_creation_futures.push(create_async_future( - id, - USER_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 1 + let id = 1; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] }, + { "op": "add", "path": "/client_side", "value": "optional" }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Some(modify_json), + )); - // Test project 2 - let id = 2; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[0..2]); - json["server_side"] = json!("required"); - json["title"] = json!("Mysterious Project"); - }; - project_creation_futures.push(create_async_future( - id, - USER_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 2 + let id = 2; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] }, + { "op": "add", "path": "/server_side", "value": "required" }, + { "op": "add", "path": "/title", "value": "Mysterious Project" }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Some(modify_json), + )); - // Test project 3 - let id = 3; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[0..3]); - json["server_side"] = json!("required"); - json["initial_versions"][0]["game_versions"] = json!(["1.20.4"]); - json["title"] = json!("Mysterious Project"); - json["license_id"] = json!("LicenseRef-All-Rights-Reserved"); // closed source - }; - project_creation_futures.push(create_async_future( - id, - FRIEND_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 3 + let id = 3; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] }, + { "op": "add", "path": "/server_side", "value": "required" }, + { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.4"] }, + { "op": "add", "path": "/title", "value": "Mysterious Project" }, + { "op": "add", "path": "/license_id", "value": "LicenseRef-All-Rights-Reserved" }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + FRIEND_USER_PAT, + false, + Some(modify_json), + )); - // Test project 4 - let id = 4; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[0..3]); - json["client_side"] = json!("optional"); - json["initial_versions"][0]["game_versions"] = json!(["1.20.5"]); - }; - project_creation_futures.push(create_async_future( - id, - USER_USER_PAT, - true, - Box::new(modify_json), - )); + // Test project 4 + let id = 4; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] }, + { "op": "add", "path": "/client_side", "value": "optional" }, + { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.5"] }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + true, + Some(modify_json), + )); - // Test project 5 - let id = 5; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[5..6]); - json["client_side"] = json!("optional"); - json["initial_versions"][0]["game_versions"] = json!(["1.20.5"]); - json["license_id"] = json!("LGPL-3.0-or-later"); - }; - project_creation_futures.push(create_async_future( - id, - USER_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 5 + let id = 5; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, + { "op": "add", "path": "/client_side", "value": "optional" }, + { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.5"] }, + { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Some(modify_json), + )); - // Test project 6 - let id = 6; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[5..6]); - json["client_side"] = json!("optional"); - json["server_side"] = json!("required"); - json["license_id"] = json!("LGPL-3.0-or-later"); - }; - project_creation_futures.push(create_async_future( - id, - FRIEND_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 6 + let id = 6; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, + { "op": "add", "path": "/client_side", "value": "optional" }, + { "op": "add", "path": "/server_side", "value": "required" }, + { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + FRIEND_USER_PAT, + false, + Some(modify_json), + )); - // Test project 7 (testing the search bug) - // This project has an initial private forge version that is 1.20.3, and a fabric 1.20.5 version. - // This means that a search for fabric + 1.20.3 or forge + 1.20.5 should not return this project. - let id = 7; - let modify_json = |json: &mut serde_json::Value| { - json["categories"] = json!(DUMMY_CATEGORIES[5..6]); - json["client_side"] = json!("optional"); - json["server_side"] = json!("required"); - json["license_id"] = json!("LGPL-3.0-or-later"); - json["initial_versions"][0]["loaders"] = json!(["forge"]); - json["initial_versions"][0]["game_versions"] = json!(["1.20.2"]); - }; - project_creation_futures.push(create_async_future( - id, - USER_USER_PAT, - false, - Box::new(modify_json), - )); + // Test project 7 (testing the search bug) + // This project has an initial private forge version that is 1.20.3, and a fabric 1.20.5 version. + // This means that a search for fabric + 1.20.3 or forge + 1.20.5 should not return this project. + let id = 7; + let modify_json = serde_json::from_value(json!([ + { "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] }, + { "op": "add", "path": "/client_side", "value": "optional" }, + { "op": "add", "path": "/server_side", "value": "required" }, + { "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" }, + { "op": "add", "path": "/initial_versions/0/loaders", "value": ["forge"] }, + { "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.2"] }, + ])) + .unwrap(); + project_creation_futures.push(create_async_future( + id, + USER_USER_PAT, + false, + Some(modify_json), + )); - // Await all project creation - // Returns a mapping of: - // project id -> test id - let id_conversion: Arc> = Arc::new( - futures::future::join_all(project_creation_futures) - .await - .into_iter() - .collect(), - ); + // Await all project creation + // Returns a mapping of: + // project id -> test id + let id_conversion: Arc> = Arc::new( + futures::future::join_all(project_creation_futures) + .await + .into_iter() + .collect(), + ); - // Create a second version for project 7 - let project_7 = api - .get_project_deserialized(&format!("{test_name}-searchable-project-7"), USER_USER_PAT) + // Create a second version for project 7 + let project_7 = api + .get_project_deserialized(&format!("{test_name}-searchable-project-7"), USER_USER_PAT) + .await; + api.add_public_version( + project_7.id, + "1.0.0", + TestFile::build_random_jar(), + None, + None, + USER_USER_PAT, + ) .await; - api.add_public_version( - get_public_version_creation_data(project_7.id, "1.0.0", TestFile::build_random_jar()), - USER_USER_PAT, - ) - .await; - // Pairs of: - // 1. vec of search facets - // 2. expected project ids to be returned by this search - let pairs = vec![ - (json!([["categories:fabric"]]), vec![0, 1, 2, 3, 4, 5, 6, 7]), - (json!([["categories:forge"]]), vec![7]), - ( - json!([["categories:fabric", "categories:forge"]]), - vec![0, 1, 2, 3, 4, 5, 6, 7], - ), - (json!([["categories:fabric"], ["categories:forge"]]), vec![]), - ( - json!([ - ["categories:fabric"], - [&format!("categories:{}", DUMMY_CATEGORIES[0])], - ]), - vec![1, 2, 3, 4], - ), - (json!([["project_types:modpack"]]), vec![4]), - (json!([["client_side:required"]]), vec![0, 2, 3, 7]), - (json!([["server_side:required"]]), vec![0, 2, 3, 6, 7]), - (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7]), - (json!([["license:MIT"]]), vec![1, 2, 4]), - (json!([[r#"title:'Mysterious Project'"#]]), vec![2, 3]), - (json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7]), - (json!([["versions:1.20.5"]]), vec![4, 5]), - // bug fix - ( - json!([ - // Only the forge one has 1.20.2, so its true that this project 'has' - // 1.20.2 and a fabric version, but not true that it has a 1.20.2 fabric version. - ["categories:fabric"], - ["versions:1.20.2"] - ]), - vec![], - ), - // Project type change - // Modpack should still be able to search based on former loader, even though technically the loader is 'mrpack' - (json!([["categories:mrpack"]]), vec![4]), - ( - json!([["categories:mrpack"], ["categories:fabric"]]), - vec![4], - ), - ( - json!([ - ["categories:mrpack"], - ["categories:fabric"], - ["project_type:modpack"] - ]), - vec![4], - ), - ]; + // Pairs of: + // 1. vec of search facets + // 2. expected project ids to be returned by this search + let pairs = vec![ + (json!([["categories:fabric"]]), vec![0, 1, 2, 3, 4, 5, 6, 7]), + (json!([["categories:forge"]]), vec![7]), + ( + json!([["categories:fabric", "categories:forge"]]), + vec![0, 1, 2, 3, 4, 5, 6, 7], + ), + (json!([["categories:fabric"], ["categories:forge"]]), vec![]), + ( + json!([ + ["categories:fabric"], + [&format!("categories:{}", DUMMY_CATEGORIES[0])], + ]), + vec![1, 2, 3, 4], + ), + (json!([["project_types:modpack"]]), vec![4]), + (json!([["client_side:required"]]), vec![0, 2, 3, 7]), + (json!([["server_side:required"]]), vec![0, 2, 3, 6, 7]), + (json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7]), + (json!([["license:MIT"]]), vec![1, 2, 4]), + (json!([[r#"title:'Mysterious Project'"#]]), vec![2, 3]), + (json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7]), + (json!([["versions:1.20.5"]]), vec![4, 5]), + // bug fix + ( + json!([ + // Only the forge one has 1.20.2, so its true that this project 'has' + // 1.20.2 and a fabric version, but not true that it has a 1.20.2 fabric version. + ["categories:fabric"], + ["versions:1.20.2"] + ]), + vec![], + ), + // Project type change + // Modpack should still be able to search based on former loader, even though technically the loader is 'mrpack' + (json!([["categories:mrpack"]]), vec![4]), + ( + json!([["categories:mrpack"], ["categories:fabric"]]), + vec![4], + ), + ( + json!([ + ["categories:mrpack"], + ["categories:fabric"], + ["project_type:modpack"] + ]), + vec![4], + ), + ]; - // TODO: Untested: - // - downloads (not varied) - // - color (not varied) - // - created_timestamp (not varied) - // - modified_timestamp (not varied) + // TODO: Untested: + // - downloads (not varied) + // - color (not varied) + // - created_timestamp (not varied) + // - modified_timestamp (not varied) - // Forcibly reset the search index - let resp = api.reset_search_index().await; - assert_eq!(resp.status(), 204); + // Forcibly reset the search index + let resp = api.reset_search_index().await; + assert_eq!(resp.status(), 204); - // Test searches - let stream = futures::stream::iter(pairs); - stream - .for_each_concurrent(1, |(facets, mut expected_project_ids)| { - let id_conversion = id_conversion.clone(); - let test_name = test_name.clone(); - async move { - let projects = api - .search_deserialized(Some(&test_name), Some(facets.clone()), USER_USER_PAT) - .await; - let mut found_project_ids: Vec = projects - .hits - .into_iter() - .map(|p| id_conversion[&parse_base62(&p.project_id).unwrap()]) - .collect(); - expected_project_ids.sort(); - found_project_ids.sort(); - assert_eq!(found_project_ids, expected_project_ids); - } - }) - .await; - - // Cleanup test db - test_env.cleanup().await; + // Test searches + let stream = futures::stream::iter(pairs); + stream + .for_each_concurrent(1, |(facets, mut expected_project_ids)| { + let id_conversion = id_conversion.clone(); + let test_name = test_name.clone(); + async move { + let projects = api + .search_deserialized_common( + Some(&test_name), + Some(facets.clone()), + USER_USER_PAT, + ) + .await; + let mut found_project_ids: Vec = projects + .hits + .into_iter() + .map(|p| id_conversion[&parse_base62(&p.project_id).unwrap()]) + .collect(); + expected_project_ids.sort(); + found_project_ids.sort(); + assert_eq!(found_project_ids, expected_project_ids); + } + }) + .await; + }) + .await; } diff --git a/tests/v2/tags.rs b/tests/v2/tags.rs index 037219d3..80c1f009 100644 --- a/tests/v2/tags.rs +++ b/tests/v2/tags.rs @@ -1,74 +1,77 @@ -use crate::common::environment::TestEnvironment; use std::collections::HashSet; +use crate::common::{ + api_v2::ApiV2, + environment::{with_test_environment, TestEnvironment}, +}; + #[actix_rt::test] async fn get_tags() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; - - let game_versions = api.get_game_versions_deserialized().await; - let loaders = api.get_loaders_deserialized().await; - let side_types = api.get_side_types_deserialized().await; - let categories = api.get_categories_deserialized().await; + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + let game_versions = api.get_game_versions_deserialized().await; + let loaders = api.get_loaders_deserialized().await; + let side_types = api.get_side_types_deserialized().await; + let categories = api.get_categories_deserialized().await; - // These tests match dummy data and will need to be updated if the dummy data changes; - let game_version_versions = game_versions - .into_iter() - .map(|x| x.version) - .collect::>(); - assert_eq!( - game_version_versions, - [ - "1.20.1", - "1.20.2", - "1.20.3", - "1.20.4", - "1.20.5", - "Ordering_Negative1", - "Ordering_Positive100" - ] - .iter() - .map(|s| s.to_string()) - .collect() - ); - - let loader_names = loaders.into_iter().map(|x| x.name).collect::>(); - assert_eq!( - loader_names, - ["fabric", "forge", "mrpack"] + // These tests match dummy data and will need to be updated if the dummy data changes; + let game_version_versions = game_versions + .into_iter() + .map(|x| x.version) + .collect::>(); + assert_eq!( + game_version_versions, + [ + "1.20.1", + "1.20.2", + "1.20.3", + "1.20.4", + "1.20.5", + "Ordering_Negative1", + "Ordering_Positive100" + ] .iter() .map(|s| s.to_string()) .collect() - ); + ); + + let loader_names = loaders.into_iter().map(|x| x.name).collect::>(); + assert_eq!( + loader_names, + ["fabric", "forge", "mrpack"] + .iter() + .map(|s| s.to_string()) + .collect() + ); - let side_type_names = side_types.into_iter().collect::>(); - assert_eq!( - side_type_names, - ["unknown", "required", "optional", "unsupported"] + let side_type_names = side_types.into_iter().collect::>(); + assert_eq!( + side_type_names, + ["unknown", "required", "optional", "unsupported"] + .iter() + .map(|s| s.to_string()) + .collect() + ); + + let category_names = categories + .into_iter() + .map(|x| x.name) + .collect::>(); + assert_eq!( + category_names, + [ + "combat", + "economy", + "food", + "optimization", + "decoration", + "mobs", + "magic" + ] .iter() .map(|s| s.to_string()) .collect() - ); - - let category_names = categories - .into_iter() - .map(|x| x.name) - .collect::>(); - assert_eq!( - category_names, - [ - "combat", - "economy", - "food", - "optimization", - "decoration", - "mobs", - "magic" - ] - .iter() - .map(|s| s.to_string()) - .collect() - ); - - test_env.cleanup().await; + ); + }) + .await; } diff --git a/tests/v2/version.rs b/tests/v2/version.rs index dfa215b2..421aa3c4 100644 --- a/tests/v2/version.rs +++ b/tests/v2/version.rs @@ -1,409 +1,466 @@ use actix_web::test; use futures::StreamExt; -use labrinth::models::projects::{ProjectId, VersionId}; +use labrinth::models::projects::VersionId; use labrinth::{ - models::{ - ids::base62_impl::parse_base62, - projects::{Loader, VersionStatus, VersionType}, - }, + models::projects::{Loader, VersionStatus, VersionType}, routes::v2::version_file::FileUpdateData, }; use serde_json::json; -use crate::common::api_v2::request_data::get_public_version_creation_data; +use crate::common::api_common::{ApiProject, ApiVersion}; +use crate::common::api_v2::ApiV2; +use crate::common::environment::{with_test_environment, TestEnvironment}; use crate::common::{ database::{ENEMY_USER_PAT, USER_USER_PAT}, dummy_data::TestFile, - environment::TestEnvironment, }; #[actix_rt::test] pub async fn test_patch_version() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; - - let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; - - // // First, we do some patch requests that should fail. - // // Failure because the user is not authorized. - let resp = api - .edit_version( - alpha_version_id, - json!({ - "name": "test 1", - }), - ENEMY_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 401); - - // Failure because these are illegal requested statuses for a normal user. - for req in ["unknown", "scheduled"] { + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + + let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; + + // // First, we do some patch requests that should fail. + // // Failure because the user is not authorized. + let resp = api + .edit_version( + alpha_version_id, + json!({ + "name": "test 1", + }), + ENEMY_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 401); + + // Failure because these are illegal requested statuses for a normal user. + for req in ["unknown", "scheduled"] { + let resp = api + .edit_version( + alpha_version_id, + json!({ + "status": req, + // requested status it not set here, but in /schedule + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Sucessful request to patch many fields. + let resp = api + .edit_version( + alpha_version_id, + json!({ + "name": "new version name", + "version_number": "1.3.0", + "changelog": "new changelog", + "version_type": "beta", + // // "dependencies": [], TODO: test this + "game_versions": ["1.20.5"], + "loaders": ["forge"], + "featured": false, + // "primary_file": [], TODO: test this + // // "downloads": 0, TODO: moderator exclusive + "status": "draft", + // // "filetypes": ["jar"], TODO: test this + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + + let version = api + .get_version_deserialized(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!(version.name, "new version name"); + assert_eq!(version.version_number, "1.3.0"); + assert_eq!(version.changelog, "new changelog"); + assert_eq!( + version.version_type, + serde_json::from_str::("\"beta\"").unwrap() + ); + assert_eq!(version.game_versions, vec!["1.20.5"]); + assert_eq!(version.loaders, vec![Loader("forge".to_string())]); + assert!(!version.featured); + assert_eq!(version.status, VersionStatus::from_string("draft")); + + // These ones are checking the v2-v3 rerouting, we eneusre that only 'game_versions' + // works as expected, as well as only 'loaders' let resp = api .edit_version( alpha_version_id, json!({ - "status": req, - // requested status it not set here, but in /schedule + "game_versions": ["1.20.1", "1.20.2", "1.20.4"], }), USER_USER_PAT, ) .await; - assert_eq!(resp.status(), 400); - } - - // Sucessful request to patch many fields. - let resp = api - .edit_version( - alpha_version_id, - json!({ - "name": "new version name", - "version_number": "1.3.0", - "changelog": "new changelog", - "version_type": "beta", - // // "dependencies": [], TODO: test this - "game_versions": ["1.20.5"], - "loaders": ["forge"], - "featured": false, - // "primary_file": [], TODO: test this - // // "downloads": 0, TODO: moderator exclusive - "status": "draft", - // // "filetypes": ["jar"], TODO: test this - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - - let version = api - .get_version_deserialized(alpha_version_id, USER_USER_PAT) - .await; - assert_eq!(version.name, "new version name"); - assert_eq!(version.version_number, "1.3.0"); - assert_eq!(version.changelog, "new changelog"); - assert_eq!( - version.version_type, - serde_json::from_str::("\"beta\"").unwrap() - ); - assert_eq!(version.game_versions, vec!["1.20.5"]); - assert_eq!(version.loaders, vec![Loader("forge".to_string())]); - assert!(!version.featured); - assert_eq!(version.status, VersionStatus::from_string("draft")); - - // These ones are checking the v2-v3 rerouting, we eneusre that only 'game_versions' - // works as expected, as well as only 'loaders' - let resp = api - .edit_version( - alpha_version_id, - json!({ - "game_versions": ["1.20.1", "1.20.2", "1.20.4"], - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - - let version = api - .get_version_deserialized(alpha_version_id, USER_USER_PAT) - .await; - assert_eq!(version.game_versions, vec!["1.20.1", "1.20.2", "1.20.4"]); - assert_eq!(version.loaders, vec![Loader("forge".to_string())]); // From last patch - - let resp = api - .edit_version( - alpha_version_id, - json!({ - "loaders": ["fabric"], - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - - let version = api - .get_version_deserialized(alpha_version_id, USER_USER_PAT) - .await; - assert_eq!(version.game_versions, vec!["1.20.1", "1.20.2", "1.20.4"]); // From last patch - assert_eq!(version.loaders, vec![Loader("fabric".to_string())]); - - // Cleanup test db - test_env.cleanup().await; + assert_eq!(resp.status(), 204); + + let version = api + .get_version_deserialized(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!(version.game_versions, vec!["1.20.1", "1.20.2", "1.20.4"]); + assert_eq!(version.loaders, vec![Loader("forge".to_string())]); // From last patch + + let resp = api + .edit_version( + alpha_version_id, + json!({ + "loaders": ["fabric"], + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + + let version = api + .get_version_deserialized(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!(version.game_versions, vec!["1.20.1", "1.20.2", "1.20.4"]); // From last patch + assert_eq!(version.loaders, vec![Loader("fabric".to_string())]); + }) + .await; } #[actix_rt::test] async fn version_updates() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v2; - - let alpha_project_id: &String = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; - let beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; - let alpha_version_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash; - let beta_version_hash = &test_env.dummy.as_ref().unwrap().project_beta.file_hash; - - // Quick test, using get version from hash - let version = api - .get_version_from_hash_deserialized(alpha_version_hash, "sha1", USER_USER_PAT) - .await; - assert_eq!(&version.id.to_string(), alpha_version_id); - - // Get versions from hash - let versions = api - .get_versions_from_hashes_deserialized( - &[alpha_version_hash.as_str(), beta_version_hash.as_str()], - "sha1", - USER_USER_PAT, - ) - .await; - assert_eq!(versions.len(), 2); - assert_eq!( - &versions[alpha_version_hash].id.to_string(), - alpha_version_id - ); - assert_eq!(&versions[beta_version_hash].id.to_string(), beta_version_id); - - // When there is only the one version, there should be no updates - let version = api - .get_update_from_hash_deserialized( - alpha_version_hash, - "sha1", - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(&version.id.to_string(), alpha_version_id); - - let versions = api - .update_files_deserialized( - "sha1", - vec![alpha_version_hash.to_string()], - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(versions.len(), 1); - assert_eq!( - &versions[alpha_version_hash].id.to_string(), - alpha_version_id - ); - - // Add 3 new versions, 1 before, and 2 after, with differing game_version/version_types/loaders - let mut update_ids = vec![]; - for (version_number, patch_value) in [ - ( - "0.9.9", - json!({ - "game_versions": ["1.20.1"], - }), - ), - ( - "1.5.0", - json!({ - "game_versions": ["1.20.3"], - "loaders": ["fabric"], - }), - ), - ( - "1.5.1", - json!({ - "game_versions": ["1.20.4"], - "loaders": ["forge"], - "version_type": "beta" - }), - ), - ] - .iter() - { + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + + let alpha_project_id: &String = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_project_id_parsed = test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .project_id_parsed; + let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; + let beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; + let alpha_version_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash; + let beta_version_hash = &test_env.dummy.as_ref().unwrap().project_beta.file_hash; + + // Quick test, using get version from hash let version = api - .add_public_version( - get_public_version_creation_data( - ProjectId(parse_base62(alpha_project_id).unwrap()), - version_number, - TestFile::build_random_jar(), - ), + .get_version_from_hash_deserialized(alpha_version_hash, "sha1", USER_USER_PAT) + .await; + assert_eq!(&version.id.to_string(), alpha_version_id); + + // Get versions from hash + let versions = api + .get_versions_from_hashes_deserialized( + &[alpha_version_hash.as_str(), beta_version_hash.as_str()], + "sha1", USER_USER_PAT, ) .await; - update_ids.push(version.id); + assert_eq!(versions.len(), 2); + assert_eq!( + &versions[alpha_version_hash].id.to_string(), + alpha_version_id + ); + assert_eq!(&versions[beta_version_hash].id.to_string(), beta_version_id); - // Patch using json - api.edit_version(&version.id.to_string(), patch_value.clone(), USER_USER_PAT) - .await; - } - - let check_expected = |game_versions: Option>, - loaders: Option>, - version_types: Option>, - result_id: Option| async move { - let (success, result_id) = match result_id { - Some(id) => (true, id), - None => (false, VersionId(0)), - }; - // get_update_from_hash - let resp = api - .get_update_from_hash( + // When there is only the one version, there should be no updates + let version = api + .get_update_from_hash_deserialized_common( alpha_version_hash, "sha1", - loaders.clone(), - game_versions.clone(), - version_types.clone(), + None, + None, + None, USER_USER_PAT, ) .await; - if success { - assert_eq!(resp.status(), 200); - let body: serde_json::Value = test::read_body_json(resp).await; - let id = body["id"].as_str().unwrap(); - assert_eq!(id, &result_id.to_string()); - } else { - assert_eq!(resp.status(), 404); - } + assert_eq!(&version.id.to_string(), alpha_version_id); - // update_files let versions = api - .update_files_deserialized( + .update_files_deserialized_common( "sha1", vec![alpha_version_hash.to_string()], - loaders.clone(), - game_versions.clone(), - version_types.clone(), + None, + None, + None, USER_USER_PAT, ) .await; - if success { - assert_eq!(versions.len(), 1); - let first = versions.iter().next().unwrap(); - assert_eq!(first.1.id, result_id); - } else { - assert_eq!(versions.len(), 0); + assert_eq!(versions.len(), 1); + assert_eq!( + &versions[alpha_version_hash].id.to_string(), + alpha_version_id + ); + + // Add 3 new versions, 1 before, and 2 after, with differing game_version/version_types/loaders + let mut update_ids = vec![]; + for (version_number, patch_value) in [ + ( + "0.9.9", + json!({ + "game_versions": ["1.20.1"], + }), + ), + ( + "1.5.0", + json!({ + "game_versions": ["1.20.3"], + "loaders": ["fabric"], + }), + ), + ( + "1.5.1", + json!({ + "game_versions": ["1.20.4"], + "loaders": ["forge"], + "version_type": "beta" + }), + ), + ] + .iter() + { + let version = api + .add_public_version_deserialized_common( + alpha_project_id_parsed, + version_number, + TestFile::build_random_jar(), + None, + None, + USER_USER_PAT, + ) + .await; + update_ids.push(version.id); + + // Patch using json + api.edit_version(&version.id.to_string(), patch_value.clone(), USER_USER_PAT) + .await; } - // update_individual_files - let hashes = vec![FileUpdateData { - hash: alpha_version_hash.to_string(), - loaders, - game_versions, - version_types: version_types.map(|v| { - v.into_iter() - .map(|v| serde_json::from_str(&format!("\"{v}\"")).unwrap()) - .collect() - }), - }]; + let check_expected = |game_versions: Option>, + loaders: Option>, + version_types: Option>, + result_id: Option| async move { + let (success, result_id) = match result_id { + Some(id) => (true, id), + None => (false, VersionId(0)), + }; + // get_update_from_hash + let resp = api + .get_update_from_hash( + alpha_version_hash, + "sha1", + loaders.clone(), + game_versions.clone(), + version_types.clone(), + USER_USER_PAT, + ) + .await; + if success { + assert_eq!(resp.status(), 200); + let body: serde_json::Value = test::read_body_json(resp).await; + let id = body["id"].as_str().unwrap(); + assert_eq!(id, &result_id.to_string()); + } else { + assert_eq!(resp.status(), 404); + } + + // update_files + let versions = api + .update_files_deserialized_common( + "sha1", + vec![alpha_version_hash.to_string()], + loaders.clone(), + game_versions.clone(), + version_types.clone(), + USER_USER_PAT, + ) + .await; + if success { + assert_eq!(versions.len(), 1); + let first = versions.iter().next().unwrap(); + assert_eq!(first.1.id, result_id); + } else { + assert_eq!(versions.len(), 0); + } + + // update_individual_files + let hashes = vec![FileUpdateData { + hash: alpha_version_hash.to_string(), + loaders, + game_versions, + version_types: version_types.map(|v| { + v.into_iter() + .map(|v| serde_json::from_str(&format!("\"{v}\"")).unwrap()) + .collect() + }), + }]; + let versions = api + .update_individual_files_deserialized("sha1", hashes, USER_USER_PAT) + .await; + if success { + assert_eq!(versions.len(), 1); + let first = versions.iter().next().unwrap(); + assert_eq!(first.1.id, result_id); + } else { + assert_eq!(versions.len(), 0); + } + }; + + let tests = vec![ + check_expected( + Some(vec!["1.20.1".to_string()]), + None, + None, + Some(update_ids[0]), + ), + check_expected( + Some(vec!["1.20.3".to_string()]), + None, + None, + Some(update_ids[1]), + ), + check_expected( + Some(vec!["1.20.4".to_string()]), + None, + None, + Some(update_ids[2]), + ), + // Loader restrictions + check_expected( + None, + Some(vec!["fabric".to_string()]), + None, + Some(update_ids[1]), + ), + check_expected( + None, + Some(vec!["forge".to_string()]), + None, + Some(update_ids[2]), + ), + // Version type restrictions + check_expected( + None, + None, + Some(vec!["release".to_string()]), + Some(update_ids[1]), + ), + check_expected( + None, + None, + Some(vec!["beta".to_string()]), + Some(update_ids[2]), + ), + // Specific combination + check_expected( + None, + Some(vec!["fabric".to_string()]), + Some(vec!["release".to_string()]), + Some(update_ids[1]), + ), + // Impossible combination + check_expected( + None, + Some(vec!["fabric".to_string()]), + Some(vec!["beta".to_string()]), + None, + ), + // No restrictions, should do the last one + check_expected(None, None, None, Some(update_ids[2])), + ]; + + // Wait on all tests, 4 at a time + futures::stream::iter(tests) + .buffer_unordered(4) + .collect::>() + .await; + + // We do a couple small tests for get_project_versions_deserialized as well + // TODO: expand this more. let versions = api - .update_individual_files_deserialized("sha1", hashes, USER_USER_PAT) + .get_project_versions_deserialized_common( + alpha_project_id, + None, + None, + None, + None, + None, + None, + USER_USER_PAT, + ) .await; - if success { - assert_eq!(versions.len(), 1); - let first = versions.iter().next().unwrap(); - assert_eq!(first.1.id, result_id); - } else { - assert_eq!(versions.len(), 0); - } - }; - - let tests = vec![ - check_expected( - Some(vec!["1.20.1".to_string()]), - None, - None, - Some(update_ids[0]), - ), - check_expected( - Some(vec!["1.20.3".to_string()]), - None, - None, - Some(update_ids[1]), - ), - check_expected( - Some(vec!["1.20.4".to_string()]), - None, - None, - Some(update_ids[2]), - ), - // Loader restrictions - check_expected( - None, - Some(vec!["fabric".to_string()]), - None, - Some(update_ids[1]), - ), - check_expected( - None, - Some(vec!["forge".to_string()]), - None, - Some(update_ids[2]), - ), - // Version type restrictions - check_expected( - None, - None, - Some(vec!["release".to_string()]), - Some(update_ids[1]), - ), - check_expected( - None, - None, - Some(vec!["beta".to_string()]), - Some(update_ids[2]), - ), - // Specific combination - check_expected( - None, - Some(vec!["fabric".to_string()]), - Some(vec!["release".to_string()]), - Some(update_ids[1]), - ), - // Impossible combination - check_expected( - None, - Some(vec!["fabric".to_string()]), - Some(vec!["beta".to_string()]), - None, - ), - // No restrictions, should do the last one - check_expected(None, None, None, Some(update_ids[2])), - ]; - - // Wait on all tests, 4 at a time - futures::stream::iter(tests) - .buffer_unordered(4) - .collect::>() - .await; - - // We do a couple small tests for get_project_versions_deserialized as well - // TODO: expand this more. - let versions = api - .get_project_versions_deserialized( - alpha_project_id, - None, - None, - None, - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(versions.len(), 4); - let versions = api - .get_project_versions_deserialized( - alpha_project_id, - None, - Some(vec!["forge".to_string()]), - None, - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(versions.len(), 1); - - // Cleanup test db - test_env.cleanup().await; + assert_eq!(versions.len(), 4); + let versions = api + .get_project_versions_deserialized_common( + alpha_project_id, + None, + Some(vec!["forge".to_string()]), + None, + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(versions.len(), 1); + }) + .await; +} + +#[actix_rt::test] +async fn add_version_project_types_v2() { + with_test_environment(None, |test_env: TestEnvironment| async move { + // Since v2 no longer keeps project_type at the project level but the version level, + // we have to test that the project_type is set correctly when adding a version, if its done in separate requests. + let api = &test_env.api; + + // Create a project in v2 with project_type = modpack, and no initial version set. + let (test_project, test_versions) = api + .add_public_project("test-modpack", None, None, USER_USER_PAT) + .await; + assert_eq!(test_versions.len(), 0); // No initial version set + + // Get as v2 project + let test_project = api + .get_project_deserialized(&test_project.slug.unwrap(), USER_USER_PAT) + .await; + assert_eq!(test_project.project_type, ""); // No project_type set, as no versions are set + // This is a known difference between older v2 ,but is acceptable. + // This would be the appropriate test on older v2: + // assert_eq!(test_project.project_type, "modpack"); + + // Create a version with a modpack file attached + let test_version = api + .add_public_version_deserialized_common( + test_project.id, + "1.0.0", + TestFile::build_random_mrpack(), + None, + None, + USER_USER_PAT, + ) + .await; + + // When we get the version as v2, it should display 'fabric' as the loader (and no project_type) + let test_version = api + .get_version_deserialized(&test_version.id.to_string(), USER_USER_PAT) + .await; + assert_eq!(test_version.loaders, vec![Loader("fabric".to_string())]); + + // When we get the project as v2, it should display 'modpack' as the project_type, and 'fabric' as the loader + let test_project = api + .get_project_deserialized(&test_project.slug.unwrap(), USER_USER_PAT) + .await; + assert_eq!(test_project.project_type, "modpack"); + assert_eq!(test_project.loaders, vec!["fabric"]); + + // When we get the version as v3, it should display 'mrpack' as the loader, and 'modpack' as the project_type + // When we get the project as v3, it should display 'modpack' as the project_type, and 'mrpack' as the loader + + // The project should be a modpack project + }) + .await; } diff --git a/tests/version.rs b/tests/version.rs index 57c5c710..dc559d7c 100644 --- a/tests/version.rs +++ b/tests/version.rs @@ -1,524 +1,536 @@ use std::collections::HashMap; +use crate::common::api_common::ApiVersion; +use crate::common::database::*; +use crate::common::dummy_data::TestFile; +use crate::common::{asserts::assert_status, get_json_val_str}; +use actix_http::StatusCode; use actix_web::test; -use common::environment::TestEnvironment; +use common::api_v3::ApiV3; +use common::asserts::assert_common_version_ids; +use common::database::USER_USER_PAT; +use common::environment::{with_test_environment, with_test_environment_all}; use futures::StreamExt; use labrinth::database::models::version_item::VERSIONS_NAMESPACE; use labrinth::models::ids::base62_impl::parse_base62; -use labrinth::models::projects::{Loader, ProjectId, VersionId, VersionStatus, VersionType}; +use labrinth::models::projects::{VersionId, VersionStatus, VersionType}; use labrinth::routes::v3::version_file::FileUpdateData; use serde_json::json; -use crate::common::api_v3::request_data::get_public_version_creation_data; -use crate::common::database::*; - -use crate::common::dummy_data::TestFile; - // importing common module. mod common; #[actix_rt::test] async fn test_get_version() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; - let alpha_project_id: &String = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; - let beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; - - // Perform request on dummy data - let version = api - .get_version_deserialized(alpha_version_id, USER_USER_PAT) - .await; - assert_eq!(&version.project_id.to_string(), alpha_project_id); - assert_eq!(&version.id.to_string(), alpha_version_id); - - let mut redis_conn = test_env.db.redis_pool.connect().await.unwrap(); - let cached_project = redis_conn - .get( - VERSIONS_NAMESPACE, - &parse_base62(alpha_version_id).unwrap().to_string(), + with_test_environment_all(None, |test_env| async move { + let api = &test_env.api; + let alpha_project_id: &String = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; + let beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; + + // Perform request on dummy data + let version = api + .get_version_deserialized_common(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!(&version.project_id.to_string(), alpha_project_id); + assert_eq!(&version.id.to_string(), alpha_version_id); + + let mut redis_conn = test_env.db.redis_pool.connect().await.unwrap(); + let cached_project = redis_conn + .get( + VERSIONS_NAMESPACE, + &parse_base62(alpha_version_id).unwrap().to_string(), + ) + .await + .unwrap() + .unwrap(); + let cached_project: serde_json::Value = serde_json::from_str(&cached_project).unwrap(); + assert_eq!( + cached_project["inner"]["project_id"], + json!(parse_base62(alpha_project_id).unwrap()) + ); + + // Request should fail on non-existent version + let resp = api.get_version("false", USER_USER_PAT).await; + assert_eq!(resp.status(), 404); + + // Similarly, request should fail on non-authorized user, on a yet-to-be-approved or hidden project, with a 404 (hiding the existence of the project) + // TODO: beta version should already be draft in dummy data, but theres a bug in finding it that + api.edit_version( + beta_version_id, + json!({ + "status": "draft" + }), + USER_USER_PAT, ) - .await - .unwrap() - .unwrap(); - let cached_project: serde_json::Value = serde_json::from_str(&cached_project).unwrap(); - assert_eq!( - cached_project["inner"]["project_id"], - json!(parse_base62(alpha_project_id).unwrap()) - ); - - // Request should fail on non-existent version - let resp = api.get_version("false", USER_USER_PAT).await; - assert_eq!(resp.status(), 404); - - // Similarly, request should fail on non-authorized user, on a yet-to-be-approved or hidden project, with a 404 (hiding the existence of the project) - // TODO: beta version should already be draft in dummy data, but theres a bug in finding it that - api.edit_version( - beta_version_id, - json!({ - "status": "draft" - }), - USER_USER_PAT, - ) + .await; + let resp = api.get_version(beta_version_id, USER_USER_PAT).await; + assert_eq!(resp.status(), 200); + let resp = api.get_version(beta_version_id, ENEMY_USER_PAT).await; + assert_eq!(resp.status(), 404); + }) .await; - let resp = api.get_version(beta_version_id, USER_USER_PAT).await; - assert_eq!(resp.status(), 200); - let resp = api.get_version(beta_version_id, ENEMY_USER_PAT).await; - assert_eq!(resp.status(), 404); - - // Cleanup test db - test_env.cleanup().await; } #[actix_rt::test] async fn version_updates() { // Test setup and dummy data - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; - - let alpha_project_id: &String = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; - let beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; - let alpha_version_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash; - let beta_version_hash = &test_env.dummy.as_ref().unwrap().project_beta.file_hash; - - // Quick test, using get version from hash - let version = api - .get_version_from_hash_deserialized(alpha_version_hash, "sha1", USER_USER_PAT) - .await; - assert_eq!(&version.id.to_string(), alpha_version_id); + with_test_environment( + None, + |test_env: common::environment::TestEnvironment| async move { + let api = &test_env.api; + + let alpha_project_id: &String = + &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_project_id_parsed = test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .project_id_parsed; + let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; + let beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; + let alpha_version_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash; + let beta_version_hash = &test_env.dummy.as_ref().unwrap().project_beta.file_hash; + + // Quick test, using get version from hash + let version = api + .get_version_from_hash_deserialized_common( + alpha_version_hash, + "sha1", + USER_USER_PAT, + ) + .await; + assert_eq!(&version.id.to_string(), alpha_version_id); + + // Get versions from hash + let versions = api + .get_versions_from_hashes_deserialized_common( + &[alpha_version_hash.as_str(), beta_version_hash.as_str()], + "sha1", + USER_USER_PAT, + ) + .await; + assert_eq!(versions.len(), 2); + assert_eq!( + &versions[alpha_version_hash].id.to_string(), + alpha_version_id + ); + assert_eq!(&versions[beta_version_hash].id.to_string(), beta_version_id); + + // When there is only the one version, there should be no updates + let version = api + .get_update_from_hash_deserialized_common( + alpha_version_hash, + "sha1", + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(&version.id.to_string(), alpha_version_id); + + let versions = api + .update_files_deserialized_common( + "sha1", + vec![alpha_version_hash.to_string()], + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(versions.len(), 1); + assert_eq!( + &versions[alpha_version_hash].id.to_string(), + alpha_version_id + ); - // Get versions from hash - let versions = api - .get_versions_from_hashes_deserialized( - &[alpha_version_hash.as_str(), beta_version_hash.as_str()], - "sha1", - USER_USER_PAT, - ) - .await; - assert_eq!(versions.len(), 2); - assert_eq!( - &versions[alpha_version_hash].id.to_string(), - alpha_version_id - ); - assert_eq!(&versions[beta_version_hash].id.to_string(), beta_version_id); - - // When there is only the one version, there should be no updates - let version = api - .get_update_from_hash_deserialized( - alpha_version_hash, - "sha1", - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(&version.id.to_string(), alpha_version_id); - - let versions = api - .update_files_deserialized( - "sha1", - vec![alpha_version_hash.to_string()], - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(versions.len(), 1); - assert_eq!( - &versions[alpha_version_hash].id.to_string(), - alpha_version_id - ); - - // Add 3 new versions, 1 before, and 2 after, with differing game_version/version_types/loaders - let mut update_ids = vec![]; - for (version_number, patch_value) in [ - ( - "0.9.9", - json!({ - "game_versions": ["1.20.1"], - }), - ), - ( - "1.5.0", - json!({ - "game_versions": ["1.20.3"], - "loaders": ["fabric"], - }), - ), - ( - "1.5.1", - json!({ - "game_versions": ["1.20.4"], - "loaders": ["forge"], - "version_type": "beta" - }), - ), - ] - .iter() - { - let version = api - .add_public_version_deserialized( - get_public_version_creation_data( - ProjectId(parse_base62(alpha_project_id).unwrap()), - version_number, - TestFile::build_random_jar(), - None::, + // Add 3 new versions, 1 before, and 2 after, with differing game_version/version_types/loaders + let mut update_ids = vec![]; + for (version_number, patch_value) in [ + ( + "0.9.9", + json!({ + "game_versions": ["1.20.1"], + }), ), - USER_USER_PAT, - ) - .await; - update_ids.push(version.id); + ( + "1.5.0", + json!({ + "game_versions": ["1.20.3"], + "loaders": ["fabric"], + }), + ), + ( + "1.5.1", + json!({ + "game_versions": ["1.20.4"], + "loaders": ["forge"], + "version_type": "beta" + }), + ), + ] + .iter() + { + let version = api + .add_public_version_deserialized( + alpha_project_id_parsed, + version_number, + TestFile::build_random_jar(), + None, + None, + USER_USER_PAT, + ) + .await; + update_ids.push(version.id); + + // Patch using json + api.edit_version(&version.id.to_string(), patch_value.clone(), USER_USER_PAT) + .await; + } + + let check_expected = |game_versions: Option>, + loaders: Option>, + version_types: Option>, + result_id: Option| async move { + let (success, result_id) = match result_id { + Some(id) => (true, id), + None => (false, VersionId(0)), + }; + // get_update_from_hash + let resp = api + .get_update_from_hash( + alpha_version_hash, + "sha1", + loaders.clone(), + game_versions.clone(), + version_types.clone(), + USER_USER_PAT, + ) + .await; + if success { + assert_eq!(resp.status(), 200); + let body: serde_json::Value = test::read_body_json(resp).await; + let id = body["id"].as_str().unwrap(); + assert_eq!(id, &result_id.to_string()); + } else { + assert_eq!(resp.status(), 404); + } + + // update_files + let versions = api + .update_files_deserialized_common( + "sha1", + vec![alpha_version_hash.to_string()], + loaders.clone(), + game_versions.clone(), + version_types.clone(), + USER_USER_PAT, + ) + .await; + if success { + assert_eq!(versions.len(), 1); + let first = versions.iter().next().unwrap(); + assert_eq!(first.1.id, result_id); + } else { + assert_eq!(versions.len(), 0); + } + + // update_individual_files + let mut loader_fields = HashMap::new(); + if let Some(game_versions) = game_versions { + loader_fields.insert( + "game_versions".to_string(), + game_versions + .into_iter() + .map(|v| json!(v)) + .collect::>(), + ); + } + + let hashes = vec![FileUpdateData { + hash: alpha_version_hash.to_string(), + loaders, + loader_fields: Some(loader_fields), + version_types: version_types.map(|v| { + v.into_iter() + .map(|v| serde_json::from_str(&format!("\"{v}\"")).unwrap()) + .collect() + }), + }]; + let versions = api + .update_individual_files_deserialized("sha1", hashes, USER_USER_PAT) + .await; + if success { + assert_eq!(versions.len(), 1); + let first = versions.iter().next().unwrap(); + assert_eq!(first.1.id, result_id); + } else { + assert_eq!(versions.len(), 0); + } + }; + + let tests = vec![ + check_expected( + Some(vec!["1.20.1".to_string()]), + None, + None, + Some(update_ids[0]), + ), + check_expected( + Some(vec!["1.20.3".to_string()]), + None, + None, + Some(update_ids[1]), + ), + check_expected( + Some(vec!["1.20.4".to_string()]), + None, + None, + Some(update_ids[2]), + ), + // Loader restrictions + check_expected( + None, + Some(vec!["fabric".to_string()]), + None, + Some(update_ids[1]), + ), + check_expected( + None, + Some(vec!["forge".to_string()]), + None, + Some(update_ids[2]), + ), + // Version type restrictions + check_expected( + None, + None, + Some(vec!["release".to_string()]), + Some(update_ids[1]), + ), + check_expected( + None, + None, + Some(vec!["beta".to_string()]), + Some(update_ids[2]), + ), + // Specific combination + check_expected( + None, + Some(vec!["fabric".to_string()]), + Some(vec!["release".to_string()]), + Some(update_ids[1]), + ), + // Impossible combination + check_expected( + None, + Some(vec!["fabric".to_string()]), + Some(vec!["beta".to_string()]), + None, + ), + // No restrictions, should do the last one + check_expected(None, None, None, Some(update_ids[2])), + ]; + + // Wait on all tests, 4 at a time + futures::stream::iter(tests) + .buffer_unordered(4) + .collect::>() + .await; + + // We do a couple small tests for get_project_versions_deserialized as well + // TODO: expand this more. + let versions = api + .get_project_versions_deserialized_common( + alpha_project_id, + None, + None, + None, + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(versions.len(), 4); + let versions = api + .get_project_versions_deserialized_common( + alpha_project_id, + None, + Some(vec!["forge".to_string()]), + None, + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(versions.len(), 1); + }, + ) + .await; +} - // Patch using json - api.edit_version(&version.id.to_string(), patch_value.clone(), USER_USER_PAT) - .await; - } - - let check_expected = |game_versions: Option>, - loaders: Option>, - version_types: Option>, - result_id: Option| async move { - let (success, result_id) = match result_id { - Some(id) => (true, id), - None => (false, VersionId(0)), - }; - // get_update_from_hash +#[actix_rt::test] +pub async fn test_patch_version() { + with_test_environment_all(None, |test_env| async move { + let api = &test_env.api; + + let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; + + // // First, we do some patch requests that should fail. + // // Failure because the user is not authorized. let resp = api - .get_update_from_hash( - alpha_version_hash, - "sha1", - loaders.clone(), - game_versions.clone(), - version_types.clone(), - USER_USER_PAT, + .edit_version( + alpha_version_id, + json!({ + "name": "test 1", + }), + ENEMY_USER_PAT, ) .await; - if success { - assert_eq!(resp.status(), 200); - let body: serde_json::Value = test::read_body_json(resp).await; - let id = body["id"].as_str().unwrap(); - assert_eq!(id, &result_id.to_string()); - } else { - assert_eq!(resp.status(), 404); + assert_eq!(resp.status(), 401); + + // Failure because these are illegal requested statuses for a normal user. + for req in ["unknown", "scheduled"] { + let resp = api + .edit_version( + alpha_version_id, + json!({ + "status": req, + // requested status it not set here, but in /schedule + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); } - // update_files - let versions = api - .update_files_deserialized( - "sha1", - vec![alpha_version_hash.to_string()], - loaders.clone(), - game_versions.clone(), - version_types.clone(), + // Sucessful request to patch many fields. + let resp = api + .edit_version( + alpha_version_id, + json!({ + "name": "new version name", + "version_number": "1.3.0", + "changelog": "new changelog", + "version_type": "beta", + // // "dependencies": [], TODO: test this + "game_versions": ["1.20.5"], + "loaders": ["forge"], + "featured": false, + // "primary_file": [], TODO: test this + // // "downloads": 0, TODO: moderator exclusive + "status": "draft", + // // "filetypes": ["jar"], TODO: test this + }), USER_USER_PAT, ) .await; - if success { - assert_eq!(versions.len(), 1); - let first = versions.iter().next().unwrap(); - assert_eq!(first.1.id, result_id); - } else { - assert_eq!(versions.len(), 0); - } - - // update_individual_files - let mut loader_fields = HashMap::new(); - if let Some(game_versions) = game_versions { - loader_fields.insert( - "game_versions".to_string(), - game_versions - .into_iter() - .map(|v| json!(v)) - .collect::>(), - ); - } + assert_eq!(resp.status(), 204); - let hashes = vec![FileUpdateData { - hash: alpha_version_hash.to_string(), - loaders, - loader_fields: Some(loader_fields), - version_types: version_types.map(|v| { - v.into_iter() - .map(|v| serde_json::from_str(&format!("\"{v}\"")).unwrap()) - .collect() - }), - }]; - let versions = api - .update_individual_files_deserialized("sha1", hashes, USER_USER_PAT) + let version = api + .get_version_deserialized_common(alpha_version_id, USER_USER_PAT) .await; - if success { - assert_eq!(versions.len(), 1); - let first = versions.iter().next().unwrap(); - assert_eq!(first.1.id, result_id); - } else { - assert_eq!(versions.len(), 0); - } - }; - - let tests = vec![ - check_expected( - Some(vec!["1.20.1".to_string()]), - None, - None, - Some(update_ids[0]), - ), - check_expected( - Some(vec!["1.20.3".to_string()]), - None, - None, - Some(update_ids[1]), - ), - check_expected( - Some(vec!["1.20.4".to_string()]), - None, - None, - Some(update_ids[2]), - ), - // Loader restrictions - check_expected( - None, - Some(vec!["fabric".to_string()]), - None, - Some(update_ids[1]), - ), - check_expected( - None, - Some(vec!["forge".to_string()]), - None, - Some(update_ids[2]), - ), - // Version type restrictions - check_expected( - None, - None, - Some(vec!["release".to_string()]), - Some(update_ids[1]), - ), - check_expected( - None, - None, - Some(vec!["beta".to_string()]), - Some(update_ids[2]), - ), - // Specific combination - check_expected( - None, - Some(vec!["fabric".to_string()]), - Some(vec!["release".to_string()]), - Some(update_ids[1]), - ), - // Impossible combination - check_expected( - None, - Some(vec!["fabric".to_string()]), - Some(vec!["beta".to_string()]), - None, - ), - // No restrictions, should do the last one - check_expected(None, None, None, Some(update_ids[2])), - ]; - - // Wait on all tests, 4 at a time - futures::stream::iter(tests) - .buffer_unordered(4) - .collect::>() - .await; - - // We do a couple small tests for get_project_versions_deserialized as well - // TODO: expand this more. - let versions = api - .get_project_versions_deserialized( - alpha_project_id, - None, - None, - None, - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(versions.len(), 4); - let versions = api - .get_project_versions_deserialized( - alpha_project_id, - None, - Some(vec!["forge".to_string()]), - None, - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(versions.len(), 1); - - // Cleanup test db - test_env.cleanup().await; -} - -#[actix_rt::test] -pub async fn test_patch_version() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; - - let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; - - // // First, we do some patch requests that should fail. - // // Failure because the user is not authorized. - let resp = api - .edit_version( - alpha_version_id, - json!({ - "name": "test 1", - }), - ENEMY_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 401); + assert_eq!(version.name, "new version name"); + assert_eq!(version.version_number, "1.3.0"); + assert_eq!(version.changelog, "new changelog"); + assert_eq!( + version.version_type, + serde_json::from_str::("\"beta\"").unwrap() + ); + assert_eq!(version.loaders, vec!["forge".to_string()]); + assert!(!version.featured); + assert_eq!(version.status, VersionStatus::from_string("draft")); - // Failure because these are illegal requested statuses for a normal user. - for req in ["unknown", "scheduled"] { + // These ones are checking the v2-v3 rerouting, we eneusre that only 'game_versions' + // works as expected, as well as only 'loaders' let resp = api .edit_version( alpha_version_id, json!({ - "status": req, - // requested status it not set here, but in /schedule + "game_versions": ["1.20.1", "1.20.2", "1.20.4"], }), USER_USER_PAT, ) .await; - assert_eq!(resp.status(), 400); - } + assert_eq!(resp.status(), 204); - // Sucessful request to patch many fields. - let resp = api - .edit_version( - alpha_version_id, - json!({ - "name": "new version name", - "version_number": "1.3.0", - "changelog": "new changelog", - "version_type": "beta", - // // "dependencies": [], TODO: test this - "game_versions": ["1.20.5"], - "loaders": ["forge"], - "featured": false, - // "primary_file": [], TODO: test this - // // "downloads": 0, TODO: moderator exclusive - "status": "draft", - // // "filetypes": ["jar"], TODO: test this - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - - let version = api - .get_version_deserialized(alpha_version_id, USER_USER_PAT) - .await; - assert_eq!(version.name, "new version name"); - assert_eq!(version.version_number, "1.3.0"); - assert_eq!(version.changelog, "new changelog"); - assert_eq!( - version.version_type, - serde_json::from_str::("\"beta\"").unwrap() - ); - assert_eq!(version.loaders, vec![Loader("forge".to_string())]); - assert!(!version.featured); - assert_eq!(version.status, VersionStatus::from_string("draft")); - - // These ones are checking the v2-v3 rerouting, we eneusre that only 'game_versions' - // works as expected, as well as only 'loaders' - let resp = api - .edit_version( - alpha_version_id, - json!({ - "game_versions": ["1.20.1", "1.20.2", "1.20.4"], - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); - - let version = api - .get_version_deserialized(alpha_version_id, USER_USER_PAT) - .await; - assert_eq!(version.loaders, vec![Loader("forge".to_string())]); // From last patch - - let resp = api - .edit_version( - alpha_version_id, - json!({ - "loaders": ["fabric"], - }), - USER_USER_PAT, - ) - .await; - assert_eq!(resp.status(), 204); + let version = api + .get_version_deserialized_common(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!(version.loaders, vec!["forge".to_string()]); // From last patch - let version = api - .get_version_deserialized(alpha_version_id, USER_USER_PAT) - .await; - assert_eq!(version.loaders, vec![Loader("fabric".to_string())]); + let resp = api + .edit_version( + alpha_version_id, + json!({ + "loaders": ["fabric"], + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); - // Cleanup test db - test_env.cleanup().await; + let version = api + .get_version_deserialized_common(alpha_version_id, USER_USER_PAT) + .await; + assert_eq!(version.loaders, vec!["fabric".to_string()]); + }) + .await; } #[actix_rt::test] pub async fn test_project_versions() { - let test_env = TestEnvironment::build(None).await; - let api = &test_env.v3; - let alpha_project_id: &String = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; - let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; - let _beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; - let _alpha_version_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash; - let _beta_version_hash = &test_env.dummy.as_ref().unwrap().project_beta.file_hash; - - let versions = api - .get_project_versions_deserialized( - alpha_project_id, - None, - None, - None, - None, - None, - None, - USER_USER_PAT, - ) - .await; - assert_eq!(versions.len(), 1); - assert_eq!(&versions[0].id.to_string(), alpha_version_id); + with_test_environment_all(None, |test_env| async move { + let api = &test_env.api; + let alpha_project_id: &String = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; - test_env.cleanup().await; + let versions = api + .get_project_versions_deserialized_common( + alpha_project_id, + None, + None, + None, + None, + None, + None, + USER_USER_PAT, + ) + .await; + assert_eq!(versions.len(), 1); + assert_eq!(&versions[0].id.to_string(), alpha_version_id); + }) + .await; } -use crate::common::{asserts::assert_status, get_json_val_str}; -use actix_http::StatusCode; -use common::{ - asserts::assert_version_ids, database::USER_USER_PAT, environment::with_test_environment, -}; #[actix_rt::test] async fn can_create_version_with_ordering() { - with_test_environment(|env| async move { - let alpha_project_id = env.dummy.as_ref().unwrap().project_alpha.project_id.clone(); + with_test_environment_all(None, |env| async move { + let alpha_project_id_parsed = env.dummy.as_ref().unwrap().project_alpha.project_id_parsed; let new_version_id = get_json_val_str( - env.v3 - .create_default_version(&alpha_project_id, Some(1), USER_USER_PAT) + env.api + .add_public_version_deserialized_common( + alpha_project_id_parsed, + "1.2.3.4", + TestFile::BasicMod, + Some(1), + None, + USER_USER_PAT, + ) .await .id, ); let versions = env - .v3 - .get_versions(vec![new_version_id.clone()], USER_USER_PAT) + .api + .get_versions_deserialized_common(vec![new_version_id.clone()], USER_USER_PAT) .await; assert_eq!(versions[0].ordering, Some(1)); }) @@ -527,18 +539,18 @@ async fn can_create_version_with_ordering() { #[actix_rt::test] async fn edit_version_ordering_works() { - with_test_environment(|env| async move { + with_test_environment_all(None, |env| async move { let alpha_version_id = env.dummy.as_ref().unwrap().project_alpha.version_id.clone(); let resp = env - .v3 + .api .edit_version_ordering(&alpha_version_id, Some(10), USER_USER_PAT) .await; assert_status(&resp, StatusCode::NO_CONTENT); let versions = env - .v3 - .get_versions(vec![alpha_version_id.clone()], USER_USER_PAT) + .api + .get_versions_deserialized_common(vec![alpha_version_id.clone()], USER_USER_PAT) .await; assert_eq!(versions[0].ordering, Some(10)); }) @@ -547,78 +559,99 @@ async fn edit_version_ordering_works() { #[actix_rt::test] async fn version_ordering_for_specified_orderings_orders_lower_order_first() { - with_test_environment(|env| async move { - let alpha_project_id = env.dummy.as_ref().unwrap().project_alpha.project_id.clone(); + with_test_environment_all(None, |env| async move { + let alpha_project_id_parsed = env.dummy.as_ref().unwrap().project_alpha.project_id_parsed; let alpha_version_id = env.dummy.as_ref().unwrap().project_alpha.version_id.clone(); let new_version_id = get_json_val_str( - env.v3 - .create_default_version(&alpha_project_id, Some(1), USER_USER_PAT) + env.api + .add_public_version_deserialized_common( + alpha_project_id_parsed, + "1.2.3.4", + TestFile::BasicMod, + Some(1), + None, + USER_USER_PAT, + ) .await .id, ); - env.v3 + env.api .edit_version_ordering(&alpha_version_id, Some(10), USER_USER_PAT) .await; let versions = env - .v3 - .get_versions( + .api + .get_versions_deserialized_common( vec![alpha_version_id.clone(), new_version_id.clone()], USER_USER_PAT, ) .await; - assert_version_ids(&versions, vec![new_version_id, alpha_version_id]); + assert_common_version_ids(&versions, vec![new_version_id, alpha_version_id]); }) .await; } #[actix_rt::test] async fn version_ordering_when_unspecified_orders_oldest_first() { - with_test_environment(|env| async move { - let alpha_project_id = &env.dummy.as_ref().unwrap().project_alpha.project_id.clone(); - let alpha_version_id = env.dummy.as_ref().unwrap().project_alpha.version_id.clone(); + with_test_environment_all(None, |env| async move { + let alpha_project_id_parsed = env.dummy.as_ref().unwrap().project_alpha.project_id_parsed; + let alpha_version_id: String = env.dummy.as_ref().unwrap().project_alpha.version_id.clone(); let new_version_id = get_json_val_str( - env.v3 - .create_default_version(alpha_project_id, None, USER_USER_PAT) + env.api + .add_public_version_deserialized_common( + alpha_project_id_parsed, + "1.2.3.4", + TestFile::BasicMod, + None, + None, + USER_USER_PAT, + ) .await .id, ); let versions = env - .v3 - .get_versions( + .api + .get_versions_deserialized_common( vec![alpha_version_id.clone(), new_version_id.clone()], USER_USER_PAT, ) .await; - assert_version_ids(&versions, vec![alpha_version_id, new_version_id]); + assert_common_version_ids(&versions, vec![alpha_version_id, new_version_id]); }) .await } #[actix_rt::test] async fn version_ordering_when_specified_orders_specified_before_unspecified() { - with_test_environment(|env| async move { - let alpha_project_id = &env.dummy.as_ref().unwrap().project_alpha.project_id.clone(); + with_test_environment_all(None, |env| async move { + let alpha_project_id_parsed = env.dummy.as_ref().unwrap().project_alpha.project_id_parsed; let alpha_version_id = env.dummy.as_ref().unwrap().project_alpha.version_id.clone(); let new_version_id = get_json_val_str( - env.v3 - .create_default_version(alpha_project_id, Some(10000), USER_USER_PAT) + env.api + .add_public_version_deserialized_common( + alpha_project_id_parsed, + "1.2.3.4", + TestFile::BasicMod, + Some(1000), + None, + USER_USER_PAT, + ) .await .id, ); - env.v3 + env.api .edit_version_ordering(&alpha_version_id, None, USER_USER_PAT) .await; let versions = env - .v3 - .get_versions( + .api + .get_versions_deserialized_common( vec![alpha_version_id.clone(), new_version_id.clone()], USER_USER_PAT, ) .await; - assert_version_ids(&versions, vec![new_version_id, alpha_version_id]); + assert_common_version_ids(&versions, vec![new_version_id, alpha_version_id]); }) .await; } From bad350e49b3c54e70f3fbfe20d5a9d6f4bbc727d Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Sat, 25 Nov 2023 17:11:38 -0800 Subject: [PATCH 3/3] Plugins (#758) * plugins; datapacks * merge fixes/changes --------- Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com> --- ...01174fbeb9e8399bdcc78ffe8f80181b217a4.json | 44 ---------------- ...7c45eb1987b63c1a21666c6ca60c52217ba4d.json | 50 +++++++++++++++++++ .../20231115105022_plugins_datapacks_v3.sql | 46 +++++++++++++++++ src/database/models/loader_fields.rs | 7 ++- src/routes/v3/tags.rs | 2 + tests/common/api_v3/tags.rs | 10 +++- tests/files/dummy_data.sql | 3 ++ tests/tags.rs | 42 +++++++++++----- tests/v2/tags.rs | 49 +++++++----------- 9 files changed, 163 insertions(+), 90 deletions(-) delete mode 100644 .sqlx/query-a796587302ae98d1af5f41696e401174fbeb9e8399bdcc78ffe8f80181b217a4.json create mode 100644 .sqlx/query-cdb2f18f826097f0f17a1f7295d7c45eb1987b63c1a21666c6ca60c52217ba4d.json create mode 100644 migrations/20231115105022_plugins_datapacks_v3.sql diff --git a/.sqlx/query-a796587302ae98d1af5f41696e401174fbeb9e8399bdcc78ffe8f80181b217a4.json b/.sqlx/query-a796587302ae98d1af5f41696e401174fbeb9e8399bdcc78ffe8f80181b217a4.json deleted file mode 100644 index 14c648d9..00000000 --- a/.sqlx/query-a796587302ae98d1af5f41696e401174fbeb9e8399bdcc78ffe8f80181b217a4.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT l.id id, l.loader loader, l.icon icon,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games\n FROM loaders l \n LEFT OUTER JOIN loaders_project_types lpt ON joining_loader_id = l.id\n LEFT OUTER JOIN project_types pt ON lpt.joining_project_type_id = pt.id\n LEFT OUTER JOIN loaders_project_types_games lptg ON lptg.loader_id = lpt.joining_loader_id AND lptg.project_type_id = lpt.joining_project_type_id\n LEFT OUTER JOIN games g ON lptg.game_id = g.id\n GROUP BY l.id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "loader", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "icon", - "type_info": "Varchar" - }, - { - "ordinal": 3, - "name": "project_types", - "type_info": "VarcharArray" - }, - { - "ordinal": 4, - "name": "games", - "type_info": "VarcharArray" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - false, - null, - null - ] - }, - "hash": "a796587302ae98d1af5f41696e401174fbeb9e8399bdcc78ffe8f80181b217a4" -} diff --git a/.sqlx/query-cdb2f18f826097f0f17a1f7295d7c45eb1987b63c1a21666c6ca60c52217ba4d.json b/.sqlx/query-cdb2f18f826097f0f17a1f7295d7c45eb1987b63c1a21666c6ca60c52217ba4d.json new file mode 100644 index 00000000..e7f4eb17 --- /dev/null +++ b/.sqlx/query-cdb2f18f826097f0f17a1f7295d7c45eb1987b63c1a21666c6ca60c52217ba4d.json @@ -0,0 +1,50 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT l.id id, l.loader loader, l.icon icon, l.metadata metadata,\n ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types,\n ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games\n FROM loaders l \n LEFT OUTER JOIN loaders_project_types lpt ON joining_loader_id = l.id\n LEFT OUTER JOIN project_types pt ON lpt.joining_project_type_id = pt.id\n LEFT OUTER JOIN loaders_project_types_games lptg ON lptg.loader_id = lpt.joining_loader_id AND lptg.project_type_id = lpt.joining_project_type_id\n LEFT OUTER JOIN games g ON lptg.game_id = g.id\n GROUP BY l.id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "loader", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "icon", + "type_info": "Varchar" + }, + { + "ordinal": 3, + "name": "metadata", + "type_info": "Jsonb" + }, + { + "ordinal": 4, + "name": "project_types", + "type_info": "VarcharArray" + }, + { + "ordinal": 5, + "name": "games", + "type_info": "VarcharArray" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false, + false, + null, + null + ] + }, + "hash": "cdb2f18f826097f0f17a1f7295d7c45eb1987b63c1a21666c6ca60c52217ba4d" +} diff --git a/migrations/20231115105022_plugins_datapacks_v3.sql b/migrations/20231115105022_plugins_datapacks_v3.sql new file mode 100644 index 00000000..27dadc46 --- /dev/null +++ b/migrations/20231115105022_plugins_datapacks_v3.sql @@ -0,0 +1,46 @@ +ALTER TABLE loaders ADD COLUMN metadata jsonb NOT NULL DEFAULT '{}'::jsonb; + +-- Set 'platform' to 'true' for all plugin loaders +-- From knossos v2 + -- pluginLoaders: ['bukkit', 'spigot', 'paper', 'purpur', 'sponge', 'folia'], + -- pluginPlatformLoaders: ['bungeecord', 'waterfall', 'velocity'], + -- allPluginLoaders: [ + -- 'bukkit', + -- 'spigot', + -- 'paper', + -- 'purpur', + -- 'sponge', + -- 'bungeecord', + -- 'waterfall', + -- 'velocity', + -- 'folia', + -- ], + -- dataPackLoaders: ['datapack'], + -- modLoaders: ['forge', 'fabric', 'quilt', 'liteloader', 'modloader', 'rift', 'neoforge'], +UPDATE loaders SET metadata = jsonb_set(metadata, '{platform}', 'false'::jsonb) WHERE loader in ('bukkit', 'spigot', 'paper', 'purpur', 'sponge', 'folia'); +UPDATE loaders SET metadata = jsonb_set(metadata, '{platform}', 'true'::jsonb) WHERE loader in ('bungeecord', 'waterfall', 'velocity'); + +INSERT INTO project_types (name) VALUES ('plugin'); +INSERT INTO project_types (name) VALUES ('datapack'); + +INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id) +SELECT l.id, pt.id +FROM loaders l +CROSS JOIN project_types pt +WHERE l.loader in ('datapack') +AND pt.name = 'datapack'; + +INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id) +SELECT l.id, pt.id +FROM loaders l +CROSS JOIN project_types pt +WHERE l.loader in ('bukkit', 'spigot', 'paper', 'purpur', 'sponge', 'bungeecord', 'waterfall', 'velocity', 'folia') +AND pt.name = 'plugin'; + +INSERT INTO loaders_project_types_games (loader_id, project_type_id, game_id) +SELECT joining_loader_id, joining_project_type_id, g.id +FROM loaders_project_types lpt +INNER JOIN project_types pt ON pt.id = lpt.joining_project_type_id +CROSS JOIN games g +WHERE g.name = 'minecraft' +AND pt.name in ('plugin', 'datapack'); diff --git a/src/database/models/loader_fields.rs b/src/database/models/loader_fields.rs index 63e1d6fd..87edf8cc 100644 --- a/src/database/models/loader_fields.rs +++ b/src/database/models/loader_fields.rs @@ -85,6 +85,7 @@ pub struct Loader { pub icon: String, pub supported_project_types: Vec, pub supported_games: Vec, // slugs + pub metadata: serde_json::Value, } impl Loader { @@ -136,7 +137,7 @@ impl Loader { let result = sqlx::query!( " - SELECT l.id id, l.loader loader, l.icon icon, + SELECT l.id id, l.loader loader, l.icon icon, l.metadata metadata, ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types, ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games FROM loaders l @@ -161,7 +162,9 @@ impl Loader { .collect(), supported_games: x .games - .unwrap_or_default() + .unwrap_or_default(), + metadata: x.metadata + })) }) .try_collect::>() diff --git a/src/routes/v3/tags.rs b/src/routes/v3/tags.rs index 8900f9c4..8fca6b51 100644 --- a/src/routes/v3/tags.rs +++ b/src/routes/v3/tags.rs @@ -84,6 +84,7 @@ pub struct LoaderData { pub name: String, pub supported_project_types: Vec, pub supported_games: Vec, + pub metadata: Value, } pub async fn loader_list( @@ -98,6 +99,7 @@ pub async fn loader_list( name: x.loader, supported_project_types: x.supported_project_types, supported_games: x.supported_games, + metadata: x.metadata, }) .collect::>(); diff --git a/tests/common/api_v3/tags.rs b/tests/common/api_v3/tags.rs index da297a77..a7f75061 100644 --- a/tests/common/api_v3/tags.rs +++ b/tests/common/api_v3/tags.rs @@ -3,8 +3,10 @@ use actix_web::{ test::{self, TestRequest}, }; use async_trait::async_trait; -use labrinth::database::models::loader_fields::LoaderFieldEnumValue; use labrinth::routes::v3::tags::GameData; +use labrinth::{ + database::models::loader_fields::LoaderFieldEnumValue, routes::v3::tags::LoaderData, +}; use crate::common::{ api_common::{ @@ -48,6 +50,12 @@ impl ApiTags for ApiV3 { } impl ApiV3 { + pub async fn get_loaders_deserialized(&self) -> Vec { + let resp = self.get_loaders().await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + pub async fn get_loader_field_variants(&self, loader_field: &str) -> ServiceResponse { let req = TestRequest::get() .uri(&format!("/v3/loader_field?loader_field={}", loader_field)) diff --git a/tests/files/dummy_data.sql b/tests/files/dummy_data.sql index 8f2e4707..e332dbfc 100644 --- a/tests/files/dummy_data.sql +++ b/tests/files/dummy_data.sql @@ -25,6 +25,9 @@ INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id) V INSERT INTO loaders (id, loader) VALUES (6, 'forge'); INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id) VALUES (6,1); +INSERT INTO loaders (id, loader, metadata) VALUES (7, 'bukkit', '{"platform":false}'::jsonb); +INSERT INTO loaders (id, loader, metadata) VALUES (8, 'waterfall', '{"platform":true}'::jsonb); + -- Adds dummies to mrpack_loaders INSERT INTO loader_field_enum_values (enum_id, value) SELECT id, 'fabric' FROM loader_field_enums WHERE enum_name = 'mrpack_loaders'; INSERT INTO loader_field_enum_values (enum_id, value) SELECT id, 'forge' FROM loader_field_enums WHERE enum_name = 'mrpack_loaders'; diff --git a/tests/tags.rs b/tests/tags.rs index 00659443..21d56b9c 100644 --- a/tests/tags.rs +++ b/tests/tags.rs @@ -1,6 +1,9 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; -use common::environment::with_test_environment_all; +use common::{ + api_v3::ApiV3, + environment::{with_test_environment, with_test_environment_all, TestEnvironment}, +}; use crate::common::api_common::ApiTags; @@ -10,18 +13,8 @@ mod common; async fn get_tags() { with_test_environment_all(None, |test_env| async move { let api = &test_env.api; - let loaders = api.get_loaders_deserialized_common().await; let categories = api.get_categories_deserialized_common().await; - let loader_names = loaders.into_iter().map(|x| x.name).collect::>(); - assert_eq!( - loader_names, - ["fabric", "forge", "mrpack"] - .iter() - .map(|s| s.to_string()) - .collect() - ); - let category_names = categories .into_iter() .map(|x| x.name) @@ -44,3 +37,28 @@ async fn get_tags() { }) .await; } + +#[actix_rt::test] +async fn get_tags_v3() { + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + let loaders = api.get_loaders_deserialized().await; + + let loader_metadata = loaders + .into_iter() + .map(|x| (x.name, x.metadata.get("platform").and_then(|x| x.as_bool()))) + .collect::>(); + let loader_names = loader_metadata.keys().cloned().collect::>(); + assert_eq!( + loader_names, + ["fabric", "forge", "mrpack", "bukkit", "waterfall"] + .iter() + .map(|s| s.to_string()) + .collect() + ); + assert_eq!(loader_metadata["fabric"], None); + assert_eq!(loader_metadata["bukkit"], Some(false)); + assert_eq!(loader_metadata["waterfall"], Some(true)); + }) + .await; +} diff --git a/tests/v2/tags.rs b/tests/v2/tags.rs index 80c1f009..cb072ba0 100644 --- a/tests/v2/tags.rs +++ b/tests/v2/tags.rs @@ -1,3 +1,5 @@ +use itertools::Itertools; + use std::collections::HashSet; use crate::common::{ @@ -12,33 +14,38 @@ async fn get_tags() { let game_versions = api.get_game_versions_deserialized().await; let loaders = api.get_loaders_deserialized().await; let side_types = api.get_side_types_deserialized().await; - let categories = api.get_categories_deserialized().await; - // These tests match dummy data and will need to be updated if the dummy data changes; + // These tests match dummy data and will need to be updated if the dummy data changes + // Versions should be ordered by: + // - ordering + // - ordering ties settled by date added to database + // - We also expect presentation of NEWEST to OLDEST + // - All null orderings are treated as older than any non-null ordering + // (for this test, the 1.20.1, etc, versions are all null ordering) let game_version_versions = game_versions .into_iter() .map(|x| x.version) - .collect::>(); + .collect::>(); assert_eq!( game_version_versions, [ - "1.20.1", - "1.20.2", - "1.20.3", - "1.20.4", - "1.20.5", "Ordering_Negative1", - "Ordering_Positive100" + "Ordering_Positive100", + "1.20.5", + "1.20.4", + "1.20.3", + "1.20.2", + "1.20.1" ] .iter() .map(|s| s.to_string()) - .collect() + .collect_vec() ); let loader_names = loaders.into_iter().map(|x| x.name).collect::>(); assert_eq!( loader_names, - ["fabric", "forge", "mrpack"] + ["fabric", "forge", "mrpack", "bukkit", "waterfall"] .iter() .map(|s| s.to_string()) .collect() @@ -52,26 +59,6 @@ async fn get_tags() { .map(|s| s.to_string()) .collect() ); - - let category_names = categories - .into_iter() - .map(|x| x.name) - .collect::>(); - assert_eq!( - category_names, - [ - "combat", - "economy", - "food", - "optimization", - "decoration", - "mobs", - "magic" - ] - .iter() - .map(|s| s.to_string()) - .collect() - ); }) .await; }