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
This commit is contained in:
Wyatt Verchere
2023-11-25 13:42:39 -08:00
committed by GitHub
parent ade8c162cd
commit 172b93d07f
51 changed files with 7573 additions and 6631 deletions

View File

@@ -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"
}

22
Cargo.lock generated
View File

@@ -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"

View File

@@ -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

View File

@@ -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<String>,
/// A list of loaders this project supports
/// A list of loaders this project supports (has a newtype struct)
pub loaders: Vec<Loader>,
// 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<i32>,
pub id: VersionId,

View File

@@ -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;
}

View File

@@ -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()

View File

@@ -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;
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| 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 = test_env
.dummy
.as_ref()
.unwrap()
.project_alpha
.project_id
.clone();
let pool = test_env.db.pool.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());
// 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<Utc>); 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)),
];
// Note: these go from most recent to least recent
let money_time_pairs: [(f64, DateTime<Utc>); 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);
}
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,
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;
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<i64>, Vec<Decimal>) = 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);
}
.execute(&pool)
.await
.unwrap();
// 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<i64>, Vec<Decimal>) = 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);
}
let day = 86400;
// Cleanup test db
test_env.cleanup().await;
// 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<i64>, Vec<Decimal>) = 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<i64>, Vec<Decimal>) = 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<Decimal>) -> Vec<f64> {
#[actix_rt::test]
pub async fn permissions_analytics_revenue() {
let test_env = TestEnvironment::build(None).await;
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| 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 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;
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",
))
};
// 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();
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",
))
};
// 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();
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;
// Cleanup test db
test_env.cleanup().await;
})
.await;
}

View File

@@ -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<CommonVersion>), slug: &str, version_jar: Option<TestFile>, modify_json: Option<json_patch::Patch>, 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<CommonProject>, 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<CommonImageData>, pat: &str],
[search_deserialized_common, SearchResults, query: Option<&str>, facets: Option<serde_json::Value>, pat: &str],
}
);
delegate_api_variant!(
#[async_trait(?Send)]
impl ApiTags for GenericApi {
[get_loaders, ServiceResponse,],
[get_loaders_deserialized_common, Vec<crate::common::api_common::models::CommonLoaderData>,],
[get_categories, ServiceResponse,],
[get_categories_deserialized_common, Vec<crate::common::api_common::models::CommonCategoryData>,],
}
);
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<crate::common::api_common::models::CommonTeamMember>, team_id: &str, pat: &str],
[get_project_members, ServiceResponse, id_or_slug: &str, pat: &str],
[get_project_members_deserialized_common, Vec<crate::common::api_common::models::CommonTeamMember>, id_or_slug: &str, pat: &str],
[get_organization_members, ServiceResponse, id_or_title: &str, pat: &str],
[get_organization_members_deserialized_common, Vec<crate::common::api_common::models::CommonTeamMember>, 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<crate::common::api_common::models::CommonNotification>, 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<ProjectPermissions>, organization_permissions: Option<OrganizationPermissions>, 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<i32>, modify_json: Option<json_patch::Patch>, pat: &str],
[add_public_version_deserialized_common, CommonVersion, project_id: ProjectId, version_number: &str, version_jar: TestFile, ordering: Option<i32>, modify_json: Option<json_patch::Patch>, 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<String>, pat: &str],
[get_versions_deserialized_common, Vec<CommonVersion>, ids_or_slugs: Vec<String>, 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<String, CommonVersion>, hashes: &[&str], algorithm: &str, pat: &str],
[get_update_from_hash, ServiceResponse, hash: &str, algorithm: &str, loaders: Option<Vec<String>>,game_versions: Option<Vec<String>>, version_types: Option<Vec<String>>, pat: &str],
[get_update_from_hash_deserialized_common, CommonVersion, hash: &str, algorithm: &str,loaders: Option<Vec<String>>,game_versions: Option<Vec<String>>,version_types: Option<Vec<String>>, pat: &str],
[update_files, ServiceResponse, algorithm: &str, hashes: Vec<String>, loaders: Option<Vec<String>>, game_versions: Option<Vec<String>>, version_types: Option<Vec<String>>, pat: &str],
[update_files_deserialized_common, HashMap<String, CommonVersion>, algorithm: &str, hashes: Vec<String>, loaders: Option<Vec<String>>, game_versions: Option<Vec<String>>, version_types: Option<Vec<String>>, pat: &str],
[get_project_versions, ServiceResponse, project_id_slug: &str, game_versions: Option<Vec<String>>,loaders: Option<Vec<String>>,featured: Option<bool>, version_type: Option<VersionType>, limit: Option<usize>, offset: Option<usize>,pat: &str],
[get_project_versions_deserialized_common, Vec<CommonVersion>, project_id_slug: &str, game_versions: Option<Vec<String>>, loaders: Option<Vec<String>>,featured: Option<bool>,version_type: Option<VersionType>,limit: Option<usize>,offset: Option<usize>,pat: &str],
[edit_version_ordering, ServiceResponse, version_id: &str,ordering: Option<i32>,pat: &str],
}
);

View File

@@ -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<TestFile>,
modify_json: Option<json_patch::Patch>,
pat: &str,
) -> (CommonProject, Vec<CommonVersion>);
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<CommonProject>;
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<CommonImageData>,
pat: &str,
) -> ServiceResponse;
async fn search_deserialized_common(
&self,
query: Option<&str>,
facets: Option<serde_json::Value>,
pat: &str,
) -> SearchResults;
}
#[async_trait(?Send)]
pub trait ApiTags {
async fn get_loaders(&self) -> ServiceResponse;
async fn get_loaders_deserialized_common(&self) -> Vec<CommonLoaderData>;
async fn get_categories(&self) -> ServiceResponse;
async fn get_categories_deserialized_common(&self) -> Vec<CommonCategoryData>;
}
#[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<CommonTeamMember>;
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<CommonTeamMember>;
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<CommonTeamMember>;
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<CommonNotification>;
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<ProjectPermissions>,
organization_permissions: Option<OrganizationPermissions>,
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<i32>,
modify_json: Option<json_patch::Patch>,
pat: &str,
) -> ServiceResponse;
async fn add_public_version_deserialized_common(
&self,
project_id: ProjectId,
version_number: &str,
version_jar: TestFile,
ordering: Option<i32>,
modify_json: Option<json_patch::Patch>,
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<String>, pat: &str) -> ServiceResponse;
async fn get_versions_deserialized_common(
&self,
ids_or_slugs: Vec<String>,
pat: &str,
) -> Vec<CommonVersion>;
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<String, CommonVersion>;
async fn get_update_from_hash(
&self,
hash: &str,
algorithm: &str,
loaders: Option<Vec<String>>,
game_versions: Option<Vec<String>>,
version_types: Option<Vec<String>>,
pat: &str,
) -> ServiceResponse;
async fn get_update_from_hash_deserialized_common(
&self,
hash: &str,
algorithm: &str,
loaders: Option<Vec<String>>,
game_versions: Option<Vec<String>>,
version_types: Option<Vec<String>>,
pat: &str,
) -> CommonVersion;
async fn update_files(
&self,
algorithm: &str,
hashes: Vec<String>,
loaders: Option<Vec<String>>,
game_versions: Option<Vec<String>>,
version_types: Option<Vec<String>>,
pat: &str,
) -> ServiceResponse;
async fn update_files_deserialized_common(
&self,
algorithm: &str,
hashes: Vec<String>,
loaders: Option<Vec<String>>,
game_versions: Option<Vec<String>>,
version_types: Option<Vec<String>>,
pat: &str,
) -> HashMap<String, CommonVersion>;
#[allow(clippy::too_many_arguments)]
async fn get_project_versions(
&self,
project_id_slug: &str,
game_versions: Option<Vec<String>>,
loaders: Option<Vec<String>>,
featured: Option<bool>,
version_type: Option<VersionType>,
limit: Option<usize>,
offset: Option<usize>,
pat: &str,
) -> ServiceResponse;
#[allow(clippy::too_many_arguments)]
async fn get_project_versions_deserialized_common(
&self,
slug: &str,
game_versions: Option<Vec<String>>,
loaders: Option<Vec<String>>,
featured: Option<bool>,
version_type: Option<VersionType>,
limit: Option<usize>,
offset: Option<usize>,
pat: &str,
) -> Vec<CommonVersion>;
async fn edit_version_ordering(
&self,
version_id: &str,
ordering: Option<i32>,
pat: &str,
) -> ServiceResponse;
}

View File

@@ -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<String>,
pub team: TeamId,
pub organization: Option<OrganizationId>,
pub title: String,
pub description: String,
pub body: String,
pub body_url: Option<String>,
pub published: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub approved: Option<DateTime<Utc>>,
pub queued: Option<DateTime<Utc>>,
pub status: ProjectStatus,
pub requested_status: Option<ProjectStatus>,
pub moderator_message: Option<ModeratorMessage>,
pub license: License,
pub downloads: u32,
pub followers: u32,
pub categories: Vec<String>,
pub additional_categories: Vec<String>,
pub loaders: Vec<String>,
pub versions: Vec<VersionId>,
pub icon_url: Option<String>,
pub issues_url: Option<String>,
pub source_url: Option<String>,
pub wiki_url: Option<String>,
pub discord_url: Option<String>,
pub donation_urls: Option<Vec<DonationLink>>,
pub gallery: Vec<GalleryItem>,
pub color: Option<u32>,
pub thread_id: ThreadId,
pub monetization_status: MonetizationStatus,
}
#[derive(Deserialize, Clone)]
pub struct CommonVersion {
pub id: VersionId,
pub loaders: Vec<String>,
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<String>,
pub date_published: DateTime<Utc>,
pub downloads: u32,
pub version_type: VersionType,
pub status: VersionStatus,
pub requested_status: Option<VersionStatus>,
pub files: Vec<VersionFile>,
pub dependencies: Vec<Dependency>,
// TODO: should ordering be in v2?
pub ordering: Option<i32>,
}
#[derive(Deserialize)]
pub struct CommonImageData {
pub filename: String,
pub extension: String,
pub icon: Vec<u8>,
}
#[derive(Deserialize)]
pub struct CommonLoaderData {
pub icon: String,
pub name: String,
pub supported_project_types: Vec<String>,
}
#[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<ProjectPermissions>,
pub organization_permissions: Option<OrganizationPermissions>,
pub accepted: bool,
pub payouts_split: Option<Decimal>,
pub ordering: i64,
}
#[derive(Deserialize)]
pub struct CommonNotification {
pub id: NotificationId,
pub user_id: UserId,
pub read: bool,
pub created: DateTime<Utc>,
pub body: NotificationBody,
// DEPRECATED: use body field instead
#[serde(rename = "type")]
pub type_: Option<String>,
pub title: String,
pub text: String,
pub link: String,
pub actions: Vec<NotificationAction>,
}

View File

@@ -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<dyn LocalService>,
}
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<dyn LocalService> = 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((

View File

@@ -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<LegacyVersion>) {
) -> Vec<LegacyProject> {
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<TestFile>,
modify_json: Option<json_patch::Patch>,
pat: &str,
) -> (CommonProject, Vec<CommonVersion>) {
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<LegacyVersion> = test::read_body_json(resp).await;
let versions: Vec<CommonVersion> = 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<LegacyProject> {
) -> Vec<CommonProject> {
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<Item = &str>,
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::<Vec<_>>()
.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<ImageData>,
icon: Option<CommonImageData>,
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<serde_json::Value>,
@@ -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<DateTime<Utc>>,
end_date: Option<DateTime<Utc>>,
resolution_minutes: Option<u32>,
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<DateTime<Utc>>,
end_date: Option<DateTime<Utc>>,
resolution_minutes: Option<u32>,
pat: &str,
) -> HashMap<String, HashMap<i64, Decimal>> {
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
}
}

View File

@@ -28,8 +28,12 @@ pub struct ImageData {
pub fn get_public_project_creation_data(
slug: &str,
version_jar: Option<TestFile>,
modify_json: Option<json_patch::Patch>,
) -> 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<i32>,
modify_json: Option<json_patch::Patch>,
) -> 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<i32>,
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!([])
};

View File

@@ -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,34 +32,6 @@ impl ApiV2 {
test::read_body_json(resp).await
}
pub async fn get_loaders(&self) -> ServiceResponse {
let req = TestRequest::get()
.uri("/v2/tag/loader")
.append_header(("Authorization", ADMIN_USER_PAT))
.to_request();
self.call(req).await
}
pub async fn get_loaders_deserialized(&self) -> Vec<LoaderData> {
let resp = self.get_loaders().await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
pub async fn get_categories(&self) -> ServiceResponse {
let req = TestRequest::get()
.uri("/v2/tag/category")
.append_header(("Authorization", ADMIN_USER_PAT))
.to_request();
self.call(req).await
}
pub async fn get_categories_deserialized(&self) -> Vec<CategoryData> {
let resp = self.get_categories().await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
pub async fn get_game_versions(&self) -> ServiceResponse {
let req = TestRequest::get()
.uri("/v2/tag/game_version")
@@ -66,4 +45,47 @@ impl ApiV2 {
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
pub async fn get_loaders_deserialized(&self) -> Vec<LoaderData> {
let resp = self.get_loaders().await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
pub async fn get_categories_deserialized(&self) -> Vec<CategoryData> {
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/loader")
.append_header(("Authorization", ADMIN_USER_PAT))
.to_request();
self.call(req).await
}
async fn get_loaders_deserialized_common(&self) -> Vec<CommonLoaderData> {
let resp = self.get_loaders().await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
async fn get_categories(&self) -> ServiceResponse {
let req = TestRequest::get()
.uri("/v2/tag/category")
.append_header(("Authorization", ADMIN_USER_PAT))
.to_request();
self.call(req).await
}
async fn get_categories_deserialized_common(&self) -> Vec<CommonCategoryData> {
let resp = self.get_categories().await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
}

View File

@@ -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<TeamMember> {
) -> Vec<CommonTeamMember> {
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<TeamMember> {
) -> Vec<CommonTeamMember> {
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<TeamMember> {
) -> Vec<CommonTeamMember> {
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<Notification> {
) -> Vec<CommonNotification> {
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))

View File

@@ -1,101 +1,39 @@
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(
&self,
creation_data: VersionCreationRequestData,
pat: &str,
) -> LegacyVersion {
// 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.get_version_deserialized(version_id, pat).await
}
pub async fn get_version(&self, id: &str, pat: &str) -> ServiceResponse {
let req = TestRequest::get()
.uri(&format!("/v2/version/{id}"))
.append_header(("Authorization", pat))
.to_request();
self.call(req).await
}
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 edit_version(
&self,
version_id: &str,
patch: serde_json::Value,
pat: &str,
) -> ServiceResponse {
let req = test::TestRequest::patch()
.uri(&format!("/v2/version/{version_id}"))
.append_header(("Authorization", pat))
.set_json(patch)
.to_request();
self.call(req).await
}
pub async fn get_version_from_hash(
&self,
hash: &str,
algorithm: &str,
pat: &str,
) -> ServiceResponse {
let req = test::TestRequest::get()
.uri(&format!("/v2/version_file/{hash}?algorithm={algorithm}"))
.append_header(("Authorization", pat))
.to_request();
self.call(req).await
}
pub async fn get_version_from_hash_deserialized(
&self,
hash: &str,
@@ -107,23 +45,6 @@ impl ApiV2 {
test::read_body_json(resp).await
}
pub async fn get_versions_from_hashes(
&self,
hashes: &[&str],
algorithm: &str,
pat: &str,
) -> ServiceResponse {
let req = TestRequest::post()
.uri("/v2/version_files")
.append_header(("Authorization", pat))
.set_json(json!({
"hashes": hashes,
"algorithm": algorithm,
}))
.to_request();
self.call(req).await
}
pub async fn get_versions_from_hashes_deserialized(
&self,
hashes: &[&str],
@@ -135,91 +56,6 @@ impl ApiV2 {
test::read_body_json(resp).await
}
pub async fn get_update_from_hash(
&self,
hash: &str,
algorithm: &str,
loaders: Option<Vec<String>>,
game_versions: Option<Vec<String>>,
version_types: Option<Vec<String>>,
pat: &str,
) -> ServiceResponse {
let req = test::TestRequest::post()
.uri(&format!(
"/v2/version_file/{hash}/update?algorithm={algorithm}"
))
.append_header(("Authorization", pat))
.set_json(json!({
"loaders": loaders,
"game_versions": game_versions,
"version_types": version_types,
}))
.to_request();
self.call(req).await
}
pub async fn get_update_from_hash_deserialized(
&self,
hash: &str,
algorithm: &str,
loaders: Option<Vec<String>>,
game_versions: Option<Vec<String>>,
version_types: Option<Vec<String>>,
pat: &str,
) -> LegacyVersion {
let resp = self
.get_update_from_hash(hash, algorithm, loaders, game_versions, version_types, pat)
.await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
pub async fn update_files(
&self,
algorithm: &str,
hashes: Vec<String>,
loaders: Option<Vec<String>>,
game_versions: Option<Vec<String>>,
version_types: Option<Vec<String>>,
pat: &str,
) -> ServiceResponse {
let req = test::TestRequest::post()
.uri("/v2/version_files/update")
.append_header(("Authorization", pat))
.set_json(json!({
"algorithm": algorithm,
"hashes": hashes,
"loaders": loaders,
"game_versions": game_versions,
"version_types": version_types,
}))
.to_request();
self.call(req).await
}
pub async fn update_files_deserialized(
&self,
algorithm: &str,
hashes: Vec<String>,
loaders: Option<Vec<String>>,
game_versions: Option<Vec<String>>,
version_types: Option<Vec<String>>,
pat: &str,
) -> HashMap<String, LegacyVersion> {
let resp = self
.update_files(
algorithm,
hashes,
loaders,
game_versions,
version_types,
pat,
)
.await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
pub async fn update_individual_files(
&self,
algorithm: &str,
@@ -247,10 +83,228 @@ impl ApiV2 {
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<i32>,
modify_json: Option<json_patch::Patch>,
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();
self.call(req).await
}
async fn add_public_version_deserialized_common(
&self,
project_id: ProjectId,
version_number: &str,
version_jar: TestFile,
ordering: Option<i32>,
modify_json: Option<json_patch::Patch>,
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
}
async fn get_version(&self, id: &str, pat: &str) -> ServiceResponse {
let req = TestRequest::get()
.uri(&format!("/v2/version/{id}"))
.append_header(("Authorization", pat))
.to_request();
self.call(req).await
}
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
}
async fn edit_version(
&self,
version_id: &str,
patch: serde_json::Value,
pat: &str,
) -> ServiceResponse {
let req = test::TestRequest::patch()
.uri(&format!("/v2/version/{version_id}"))
.append_header(("Authorization", pat))
.set_json(patch)
.to_request();
self.call(req).await
}
async fn get_version_from_hash(
&self,
hash: &str,
algorithm: &str,
pat: &str,
) -> ServiceResponse {
let req = test::TestRequest::get()
.uri(&format!("/v2/version_file/{hash}?algorithm={algorithm}"))
.append_header(("Authorization", pat))
.to_request();
self.call(req).await
}
async fn get_version_from_hash_deserialized_common(
&self,
hash: &str,
algorithm: &str,
pat: &str,
) -> CommonVersion {
let resp = self.get_version_from_hash(hash, algorithm, pat).await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
async fn get_versions_from_hashes(
&self,
hashes: &[&str],
algorithm: &str,
pat: &str,
) -> ServiceResponse {
let req = TestRequest::post()
.uri("/v2/version_files")
.append_header(("Authorization", pat))
.set_json(json!({
"hashes": hashes,
"algorithm": algorithm,
}))
.to_request();
self.call(req).await
}
async fn get_versions_from_hashes_deserialized_common(
&self,
hashes: &[&str],
algorithm: &str,
pat: &str,
) -> HashMap<String, CommonVersion> {
let resp = self.get_versions_from_hashes(hashes, algorithm, pat).await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
async fn get_update_from_hash(
&self,
hash: &str,
algorithm: &str,
loaders: Option<Vec<String>>,
game_versions: Option<Vec<String>>,
version_types: Option<Vec<String>>,
pat: &str,
) -> ServiceResponse {
let req = test::TestRequest::post()
.uri(&format!(
"/v2/version_file/{hash}/update?algorithm={algorithm}"
))
.append_header(("Authorization", pat))
.set_json(json!({
"loaders": loaders,
"game_versions": game_versions,
"version_types": version_types,
}))
.to_request();
self.call(req).await
}
async fn get_update_from_hash_deserialized_common(
&self,
hash: &str,
algorithm: &str,
loaders: Option<Vec<String>>,
game_versions: Option<Vec<String>>,
version_types: Option<Vec<String>>,
pat: &str,
) -> CommonVersion {
let resp = self
.get_update_from_hash(hash, algorithm, loaders, game_versions, version_types, pat)
.await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
async fn update_files(
&self,
algorithm: &str,
hashes: Vec<String>,
loaders: Option<Vec<String>>,
game_versions: Option<Vec<String>>,
version_types: Option<Vec<String>>,
pat: &str,
) -> ServiceResponse {
let req = test::TestRequest::post()
.uri("/v2/version_files/update")
.append_header(("Authorization", pat))
.set_json(json!({
"algorithm": algorithm,
"hashes": hashes,
"loaders": loaders,
"game_versions": game_versions,
"version_types": version_types,
}))
.to_request();
self.call(req).await
}
async fn update_files_deserialized_common(
&self,
algorithm: &str,
hashes: Vec<String>,
loaders: Option<Vec<String>>,
game_versions: Option<Vec<String>>,
version_types: Option<Vec<String>>,
pat: &str,
) -> HashMap<String, CommonVersion> {
let resp = self
.update_files(
algorithm,
hashes,
loaders,
game_versions,
version_types,
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<Vec<String>>,
@@ -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<Vec<String>>,
@@ -310,7 +364,7 @@ impl ApiV2 {
limit: Option<usize>,
offset: Option<usize>,
pat: &str,
) -> Vec<LegacyVersion> {
) -> Vec<CommonVersion> {
let resp = self
.get_project_versions(
slug,
@@ -327,66 +381,7 @@ impl ApiV2 {
test::read_body_json(resp).await
}
// TODO: remove redundancy in these functions- some are essentially repeats
pub async fn create_default_version(
&self,
project_id: &str,
ordering: Option<i32>,
pat: &str,
) -> LegacyVersion {
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"] ,
"release_channel": "release",
"loaders": ["fabric"],
"featured": true,
"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
}
pub async fn get_versions(&self, version_ids: Vec<String>, pat: &str) -> Vec<LegacyVersion> {
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
}
pub async fn edit_version_ordering(
async fn edit_version_ordering(
&self,
version_id: &str,
ordering: Option<i32>,
@@ -403,4 +398,23 @@ impl ApiV2 {
.to_request();
self.call(request).await
}
async fn get_versions(&self, version_ids: Vec<String>, 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();
self.call(request).await
}
async fn get_versions_deserialized_common(
&self,
version_ids: Vec<String>,
pat: &str,
) -> Vec<CommonVersion> {
let resp = self.get_versions(version_ids, pat).await;
assert_status(&resp, StatusCode::OK);
test::read_body_json(resp).await
}
}

View File

@@ -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<dyn LocalService>,
}
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<dyn LocalService> = 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((

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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<TestFile>,
modify_json: Option<json_patch::Patch>,
pat: &str,
) -> (Project, Vec<Version>) {
) -> (CommonProject, Vec<CommonVersion>) {
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<Version> = test::read_body_json(resp).await;
let versions: Vec<CommonVersion> = 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<Project> {
) -> Vec<CommonProject> {
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<Item = &str>,
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::<Vec<_>>()
.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<ImageData>,
icon: Option<CommonImageData>,
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<serde_json::Value>,
@@ -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>,

View File

@@ -28,8 +28,12 @@ pub struct ImageData {
pub fn get_public_project_creation_data(
slug: &str,
version_jar: Option<TestFile>,
modify_json: Option<json_patch::Patch>,
) -> 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<i32>,
// closure that takes in a &mut serde_json::Value
// and modifies it before it is serialized and sent
modify_json: Option<impl FnOnce(&mut serde_json::Value)>,
modify_json: Option<json_patch::Patch>,
) -> 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<i32>,
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!([])
};

View File

@@ -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<LoaderData> {
async fn get_loaders_deserialized_common(&self) -> Vec<CommonLoaderData> {
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<CategoryData> {
async fn get_categories_deserialized_common(&self) -> Vec<CommonCategoryData> {
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))

View File

@@ -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<TeamMember> {
) -> Vec<CommonTeamMember> {
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<TeamMember> {
) -> Vec<CommonTeamMember> {
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<TeamMember> {
) -> Vec<CommonTeamMember> {
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<Notification> {
) -> Vec<CommonNotification> {
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))

View File

@@ -1,59 +1,58 @@
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(
&self,
creation_data: VersionCreationRequestData,
pat: &str,
) -> ServiceResponse {
// Add a project.
let req = TestRequest::post()
.uri("/v3/version")
.append_header(("Authorization", pat))
.set_multipart(creation_data.segment_data)
.to_request();
self.call(req).await
}
pub async fn add_public_version_deserialized(
&self,
creation_data: VersionCreationRequestData,
project_id: ProjectId,
version_number: &str,
version_jar: TestFile,
ordering: Option<i32>,
modify_json: Option<json_patch::Patch>,
pat: &str,
) -> Version {
let resp = self.add_public_version(creation_data, pat).await;
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
}
pub async fn get_version(&self, id: &str, pat: &str) -> ServiceResponse {
let req = TestRequest::get()
.uri(&format!("/v3/version/{id}"))
.append_header(("Authorization", pat))
.to_request();
self.call(req).await
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 {
@@ -62,7 +61,101 @@ impl ApiV3 {
test::read_body_json(resp).await
}
pub async fn edit_version(
pub async fn update_individual_files(
&self,
algorithm: &str,
hashes: Vec<FileUpdateData>,
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<FileUpdateData>,
pat: &str,
) -> HashMap<String, Version> {
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<i32>,
modify_json: Option<json_patch::Patch>,
pat: &str,
) -> ServiceResponse {
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))
.set_multipart(creation_data.segment_data)
.to_request();
self.call(req).await
}
async fn add_public_version_deserialized_common(
&self,
project_id: ProjectId,
version_number: &str,
version_jar: TestFile,
ordering: Option<i32>,
modify_json: Option<json_patch::Patch>,
pat: &str,
) -> CommonVersion {
let resp = self
.add_public_version(
project_id,
version_number,
version_jar,
ordering,
modify_json,
pat,
)
.await;
assert_status(&resp, StatusCode::OK);
test::read_body_json(resp).await
}
async fn get_version(&self, id: &str, pat: &str) -> ServiceResponse {
let req = TestRequest::get()
.uri(&format!("/v3/version/{id}"))
.append_header(("Authorization", pat))
.to_request();
self.call(req).await
}
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
}
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<String, Version> {
) -> HashMap<String, CommonVersion> {
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<Vec<String>>,
version_types: Option<Vec<String>>,
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<String>,
@@ -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<String>,
@@ -218,7 +311,7 @@ impl ApiV3 {
game_versions: Option<Vec<String>>,
version_types: Option<Vec<String>>,
pat: &str,
) -> HashMap<String, Version> {
) -> HashMap<String, CommonVersion> {
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<FileUpdateData>,
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<FileUpdateData>,
pat: &str,
) -> HashMap<String, Version> {
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<Vec<String>>,
@@ -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<Vec<String>>,
@@ -323,7 +388,7 @@ impl ApiV3 {
limit: Option<usize>,
offset: Option<usize>,
pat: &str,
) -> Vec<Version> {
) -> Vec<CommonVersion> {
let resp = self
.get_project_versions(
slug,
@@ -341,68 +406,7 @@ impl ApiV3 {
}
// TODO: remove redundancy in these functions
pub async fn create_default_version(
&self,
project_id: &str,
ordering: Option<i32>,
pat: &str,
) -> Version {
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,
"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
}
pub async fn get_versions(&self, version_ids: Vec<String>, pat: &str) -> Vec<Version> {
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
}
pub async fn edit_version_ordering(
async fn edit_version_ordering(
&self,
version_id: &str,
ordering: Option<i32>,
@@ -419,4 +423,23 @@ impl ApiV3 {
.to_request();
self.call(request).await
}
async fn get_versions(&self, version_ids: Vec<String>, 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();
self.call(request).await
}
async fn get_versions_deserialized_common(
&self,
version_ids: Vec<String>,
pat: &str,
) -> Vec<CommonVersion> {
let resp = self.get_versions(version_ids, pat).await;
assert_status(&resp, StatusCode::OK);
test::read_body_json(resp).await
}
}

View File

@@ -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<String>) {
assert_eq!(version_ids, expected_ids);
}
pub fn assert_common_version_ids(versions: &[CommonVersion], expected_ids: Vec<String>) {
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,

View File

@@ -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::<ApiV3>::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;

View File

@@ -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<Version> = test::read_body_json(resp).await;
let resp = api.call(req).await;
let versions: Vec<CommonVersion> = 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<Version> = test::read_body_json(resp).await;
let versions: Vec<CommonVersion> = 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()

View File

@@ -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<Fut>(f: impl FnOnce(TestEnvironment) -> Fut)
where
pub async fn with_test_environment<Fut, A>(
max_connections: Option<u32>,
f: impl FnOnce(TestEnvironment<A>) -> Fut,
) where
Fut: Future<Output = ()>,
A: ApiBuildable + 'static,
{
let test_env = TestEnvironment::build(None).await;
let test_env: TestEnvironment<A> = 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<Fut, F>(max_connections: Option<u32>, f: F)
where
Fut: Future<Output = ()>,
F: Fn(TestEnvironment<GenericApi>) -> Fut,
{
println!("Test environment: API v3");
let test_env_api_v3 = TestEnvironment::<ApiV3>::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::<ApiV2>::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<dyn LocalService>, // Rc as it's not Send
pub struct TestEnvironment<A> {
// test_app: Rc<dyn LocalService>, // Rc as it's not Send
pub db: TemporaryDatabase,
pub v2: ApiV2,
pub v3: ApiV3,
pub dummy: Option<Arc<dummy_data::DummyData>>,
pub api: A,
pub setup_api: ApiV3, // Used for setting up tests only (ie: in ScopesTest)
pub dummy: Option<dummy_data::DummyData>,
}
impl TestEnvironment {
pub async fn build(max_connections: Option<u32>) -> Self {
impl<A: ApiBuildable> TestEnvironment<A> {
async fn build(max_connections: Option<u32>) -> 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<dyn LocalService> = 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<A: Api> TestEnvironment<A> {
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);
}
}

View File

@@ -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;

View File

@@ -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<dyn Fn(&serde_json::Value) + Send>;
pub struct PermissionsTest<'a, A: Api> {
test_env: &'a TestEnvironment<A>,
// 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<ProjectPermissions>,
@@ -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<Box<dyn Fn(&serde_json::Value) + Send>>,
success_json_check: Option<Box<dyn Fn(&serde_json::Value) + Send>>,
failure_json_check: Option<JsonCheck>,
success_json_check: Option<JsonCheck>,
}
pub struct PermissionsTestContext<'a> {
pub test_env: &'a TestEnvironment,
// pub test_env: &'a TestEnvironment<A>,
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<A>) -> 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<ProjectPermissions>,
organization_permissions: Option<OrganizationPermissions>,
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<ProjectPermissions>,
organization_permissions: Option<OrganizationPermissions>,
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 {

View File

@@ -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<A>,
// 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<Scopes>,
@@ -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<A>) -> Self {
Self {
test_env,
failure_scopes: None,

View File

@@ -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<ApiV3>| 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;
}

View File

@@ -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<ApiV3>| 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);
// 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 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);
// 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<String, serde_json::Value> = 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);
// - Patch
let resp = api
.edit_version(
alpha_version_id,
json!({
"game_versions": []
}),
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"]),
] {
// 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| {
let j: &mut serde_json::Map<String, serde_json::Value> = 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 = 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 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 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;
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);
// 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 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);
// 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<String, serde_json::Value> = 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"])
);
// - 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);
}
test_env.cleanup().await;
// 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));
// 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<ApiV3>| 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;
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::<Vec<_>>();
assert_eq!(
game_version_versions,
[
"Ordering_Negative1",
"Ordering_Positive100",
"1.20.5",
"1.20.4",
"1.20.3",
"1.20.2",
"1.20.1"
]
);
// 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::<Vec<_>>();
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 side_type_names = side_types
.into_iter()
.map(|x| x.value)
.collect::<HashSet<_>>();
assert_eq!(
side_type_names,
["unknown", "required", "optional", "unsupported"]
.iter()
.map(|s| s.to_string())
.collect()
);
test_env.cleanup().await;
let side_type_names = side_types
.into_iter()
.map(|x| x.value)
.collect::<HashSet<_>>();
assert_eq!(
side_type_names,
["unknown", "required", "optional", "unsupported"]
.iter()
.map(|s| s.to_string())
.collect()
);
})
.await
}

View File

@@ -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());
})

View File

@@ -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<ApiV3>| 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<ApiV3>| 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<ApiV3>| 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<ApiV3>| 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<ApiV3>| 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<ApiV3>| 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<ApiV3>| 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<ApiV3>| 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);

View File

@@ -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<ApiV3>| 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<ApiV3>| 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<ApiV3>| 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<ApiV3>| 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<ApiV3>| 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<ApiV3>| 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<ApiV3>| 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);
})

File diff suppressed because it is too large Load Diff

View File

@@ -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;
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();
// 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();
// 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();
// 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;
// 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;
// Ensure access token is NOT returned for any PATs
for pat in success.as_array().unwrap() {
assert!(pat["access_token"].as_str().is_none());
}
// 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()
}
};
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
// 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);
// 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 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!({
"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;
// Ensure access token is NOT returned for any PATs
for pat in success.as_array().unwrap() {
assert!(pat["access_token"].as_str().is_none());
}
// 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()
}
};
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": scope.bits(),
"scopes": 0,
}))
.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(), 204);
assert_eq!(mock_pat_test(access_token).await, 401); // No longer works
// 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);
// 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);
// Cleanup test db
test_env.cleanup().await;
// 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 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!({
"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;
}
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 }
);
}
// 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),
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(
resp.status().as_u16(),
if scope.is_restricted() { 400 } else { 200 }
);
}
// 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();
// 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,
"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);
}
// 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;
// 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 }
);
}
// 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();
// 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;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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<ApiV3>| 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<dyn Fn(&mut serde_json::Value)>| {
let slug = format!("{test_name}-searchable-project-{id}");
let create_async_future =
|id: u64,
pat: &'static str,
is_modpack: bool,
modify_json: Option<json_patch::Patch>| {
let slug = format!("{test_name}-searchable-project-{id}");
let jar = if is_modpack {
TestFile::build_random_mrpack()
} else {
TestFile::build_random_jar()
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)
}
};
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;
// 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 = 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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),
));
// 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),
));
// Await all project creation
// Returns a mapping of:
// project id -> test id
let id_conversion: Arc<HashMap<u64, u64>> = 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<HashMap<u64, u64>> = 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::<fn(&mut serde_json::Value)>,
),
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
// 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<u64> = 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);
}
})
None,
None,
USER_USER_PAT,
)
.await;
// Cleanup test db
test_env.cleanup().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
// 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_common(
Some(&test_name),
Some(facets.clone()),
USER_USER_PAT,
)
.await;
let mut found_project_ids: Vec<u64> = 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;
}

View File

@@ -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::<HashSet<_>>();
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::<HashSet<_>>();
assert_eq!(
loader_names,
["fabric", "forge", "mrpack"]
let category_names = categories
.into_iter()
.map(|x| x.name)
.collect::<HashSet<_>>();
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::<HashSet<_>>();
assert_eq!(
category_names,
[
"combat",
"economy",
"food",
"optimization",
"decoration",
"mobs",
"magic"
]
.iter()
.map(|s| s.to_string())
.collect()
);
test_env.cleanup().await;
);
})
.await;
}

File diff suppressed because it is too large Load Diff

View File

@@ -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()

View File

@@ -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;
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
let api = &test_env.api;
// 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();
// 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();
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:
// 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 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"]
);
}
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"]
);
}
// 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.
// 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;
}
#[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;
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| 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"
}
);
// 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
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
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 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(),
),
};
// 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 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(),
),
};
// 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;
// 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);
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];
// 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);
// 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();
// 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);
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();
// 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);
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();
// 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);
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();
// 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);
// 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_conn = test_env.db.redis_pool.connect().await.unwrap();
assert_eq!(
redis_conn
.get(PROJECTS_SLUGS_NAMESPACE, "demo")
.await
.unwrap()
.map(|x| x.parse::<i64>().unwrap()),
None
);
assert_eq!(
redis_conn
.get(PROJECTS_SLUGS_NAMESPACE, &id)
.await
.unwrap()
.map(|x| x.parse::<i64>().unwrap()),
None
);
// 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::<i64>().unwrap()),
None
);
assert_eq!(
redis_conn
.get(PROJECTS_SLUGS_NAMESPACE, &id)
.await
.unwrap()
.map(|x| x.parse::<i64>().unwrap()),
None
);
// Old slug no longer works
let resp = api.get_project("demo", USER_USER_PAT).await;
assert_eq!(resp.status(), 404);
// Cleanup test db
test_env.cleanup().await;
// 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;
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| 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;
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([
// 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();
};
PermissionsTest::new(&test_env)
.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();
// 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();
// 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))
};
// 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();
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 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))
};
// 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();
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;
// 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;
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
let api = &test_env.api;
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;
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"] {
// 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!({
url_type: "w.fake.url",
"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 these are illegal requested statuses for a normal user.
for req in ["unknown", "processing", "withheld", "scheduled"] {
// 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);
}
// 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!({
"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",
"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);
// Old slug no longer works
let resp = api.get_project(alpha_project_slug, USER_USER_PAT).await;
assert_eq!(resp.status(), 404);
// 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);
// New slug does work
let project = api.get_project_deserialized("newslug", USER_USER_PAT).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");
// 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.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;
}

View File

@@ -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<ApiV2>| 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/version")
.set_multipart(vec![json_segment.clone(), file_segment.clone()])
};
ScopeTest::new(&test_env)
.test(req_gen, create_version)
.await
.unwrap();
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();
// Cleanup test db
test_env.cleanup().await;
// 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;
}

View File

@@ -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<ApiV2>| 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<dyn Fn(&mut serde_json::Value)>| {
let slug = format!("{test_name}-searchable-project-{id}");
let create_async_future =
|id: u64,
pat: &'static str,
is_modpack: bool,
modify_json: Option<json_patch::Patch>| {
let slug = format!("{test_name}-searchable-project-{id}");
let jar = if is_modpack {
TestFile::build_random_mrpack()
} else {
TestFile::build_random_jar()
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)
}
};
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;
// 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),
));
// 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 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 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 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 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 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 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 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 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 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 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 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 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 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),
));
// 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),
));
// Await all project creation
// Returns a mapping of:
// project id -> test id
let id_conversion: Arc<HashMap<u64, u64>> = Arc::new(
futures::future::join_all(project_creation_futures)
.await
.into_iter()
.collect(),
);
// 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),
));
// Await all project creation
// Returns a mapping of:
// project id -> test id
let id_conversion: Arc<HashMap<u64, u64>> = 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,
)
// 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)
// 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_common(
Some(&test_name),
Some(facets.clone()),
USER_USER_PAT,
)
.await;
let mut found_project_ids: Vec<u64> = 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;
// 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)
// 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<u64> = 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;
}

View File

@@ -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;
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| 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;
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::<HashSet<_>>();
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::<HashSet<_>>();
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::<HashSet<_>>();
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 side_type_names = side_types.into_iter().collect::<HashSet<_>>();
assert_eq!(
side_type_names,
["unknown", "required", "optional", "unsupported"]
let loader_names = loaders.into_iter().map(|x| x.name).collect::<HashSet<_>>();
assert_eq!(
loader_names,
["fabric", "forge", "mrpack"]
.iter()
.map(|s| s.to_string())
.collect()
);
let side_type_names = side_types.into_iter().collect::<HashSet<_>>();
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::<HashSet<_>>();
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::<HashSet<_>>();
assert_eq!(
category_names,
[
"combat",
"economy",
"food",
"optimization",
"decoration",
"mobs",
"magic"
]
.iter()
.map(|s| s.to_string())
.collect()
);
test_env.cleanup().await;
);
})
.await;
}

View File

@@ -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;
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
let api = &test_env.api;
let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id;
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"] {
// // 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!({
"status": req,
// requested status it not set here, but in /schedule
"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(), 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::<VersionType>("\"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"));
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::<VersionType>("\"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);
// 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 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 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;
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;
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| 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;
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 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(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()
{
// 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<Vec<String>>,
loaders: Option<Vec<String>>,
version_types: Option<Vec<String>>,
result_id: Option<VersionId>| 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 versions = api
.update_individual_files_deserialized("sha1", hashes, USER_USER_PAT)
let check_expected = |game_versions: Option<Vec<String>>,
loaders: Option<Vec<String>>,
version_types: Option<Vec<String>>,
result_id: Option<VersionId>| 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::<Vec<_>>()
.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::<Vec<_>>()
.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;
// 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;
}
#[actix_rt::test]
async fn add_version_project_types_v2() {
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| 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;
}

File diff suppressed because it is too large Load Diff