You've already forked AstralRinth
forked from didirus/AstralRinth
move to monorepo dir
This commit is contained in:
230
apps/labrinth/tests/analytics.rs
Normal file
230
apps/labrinth/tests/analytics.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use common::permissions::PermissionsTest;
|
||||
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;
|
||||
use labrinth::queue::payouts;
|
||||
use rust_decimal::{prelude::ToPrimitive, Decimal};
|
||||
|
||||
mod common;
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn analytics_revenue() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let api = &test_env.api;
|
||||
|
||||
let alpha_project_id = test_env.dummy.project_alpha.project_id.clone();
|
||||
|
||||
let pool = test_env.db.pool.clone();
|
||||
|
||||
// Generate sample revenue data- directly insert into sql
|
||||
let (
|
||||
mut insert_user_ids,
|
||||
mut insert_project_ids,
|
||||
mut insert_payouts,
|
||||
mut insert_starts,
|
||||
mut insert_availables,
|
||||
) = (Vec::new(), 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)),
|
||||
];
|
||||
|
||||
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);
|
||||
insert_availables.push(*time);
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await.unwrap();
|
||||
payouts::insert_payouts(
|
||||
insert_user_ids,
|
||||
insert_project_ids,
|
||||
insert_payouts,
|
||||
insert_starts,
|
||||
insert_availables,
|
||||
&mut transaction,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
transaction.commit().await.unwrap();
|
||||
|
||||
let day = 86400;
|
||||
|
||||
// Test analytics endpoint with default values
|
||||
// - all time points in the last 2 weeks
|
||||
// - 1 day resolution
|
||||
let analytics = api
|
||||
.get_analytics_revenue_deserialized(
|
||||
vec![&alpha_project_id],
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(analytics.len(), 1); // 1 project
|
||||
let project_analytics = analytics.get(&alpha_project_id).unwrap();
|
||||
assert_eq!(project_analytics.len(), 8); // 1 days cut off, and 2 points take place on the same day. note that the day exactly 14 days ago is included
|
||||
// sorted_by_key, values in the order of smallest to largest key
|
||||
let (sorted_keys, sorted_by_key): (Vec<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 {
|
||||
d.round_dp_with_strategy(1, rust_decimal::RoundingStrategy::MidpointAwayFromZero)
|
||||
.to_f64()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn to_f64_vec_rounded_up(d: Vec<Decimal>) -> Vec<f64> {
|
||||
d.into_iter().map(to_f64_rounded_up).collect_vec()
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn permissions_analytics_revenue() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let alpha_project_id = test_env.dummy.project_alpha.project_id.clone();
|
||||
let alpha_version_id = test_env.dummy.project_alpha.version_id.clone();
|
||||
let alpha_team_id = test_env.dummy.project_alpha.team_id.clone();
|
||||
|
||||
let api = &test_env.api;
|
||||
|
||||
let view_analytics = ProjectPermissions::VIEW_ANALYTICS;
|
||||
|
||||
// first, do check with a project
|
||||
let req_gen = |ctx: PermissionsTestContext| async move {
|
||||
let project_id = ctx.project_id.unwrap();
|
||||
let ids_or_slugs = vec![project_id.as_str()];
|
||||
api.get_analytics_revenue(
|
||||
ids_or_slugs,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
Some(5),
|
||||
ctx.test_pat.as_deref(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
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 = |ctx: PermissionsTestContext| {
|
||||
let alpha_version_id = alpha_version_id.clone();
|
||||
async move {
|
||||
let ids_or_slugs = vec![alpha_version_id.as_str()];
|
||||
api.get_analytics_revenue(
|
||||
ids_or_slugs,
|
||||
true,
|
||||
None,
|
||||
None,
|
||||
Some(5),
|
||||
ctx.test_pat.as_deref(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
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(), 0);
|
||||
},
|
||||
)
|
||||
.simple_project_permissions_test(view_analytics, req_gen)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Cleanup test db
|
||||
test_env.cleanup().await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
171
apps/labrinth/tests/common/api_common/generic.rs
Normal file
171
apps/labrinth/tests/common/api_common/generic.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_web::dev::ServiceResponse;
|
||||
use async_trait::async_trait;
|
||||
use labrinth::models::{
|
||||
projects::{ProjectId, VersionType},
|
||||
teams::{OrganizationPermissions, ProjectPermissions},
|
||||
};
|
||||
|
||||
use crate::common::{api_v2::ApiV2, api_v3::ApiV3, dummy_data::TestFile};
|
||||
|
||||
use super::{
|
||||
models::{CommonProject, CommonVersion},
|
||||
request_data::{ImageData, ProjectCreationRequestData},
|
||||
Api, ApiProject, ApiTags, ApiTeams, ApiUser, 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: Option<&str>],
|
||||
[get_public_project_creation_data_json, serde_json::Value, slug: &str, version_jar: Option<&TestFile>],
|
||||
[create_project, ServiceResponse, creation_data: ProjectCreationRequestData, pat: Option<&str>],
|
||||
[remove_project, ServiceResponse, project_slug_or_id: &str, pat: Option<&str>],
|
||||
[get_project, ServiceResponse, id_or_slug: &str, pat: Option<&str>],
|
||||
[get_project_deserialized_common, CommonProject, id_or_slug: &str, pat: Option<&str>],
|
||||
[get_projects, ServiceResponse, ids_or_slugs: &[&str], pat: Option<&str>],
|
||||
[get_project_dependencies, ServiceResponse, id_or_slug: &str, pat: Option<&str>],
|
||||
[get_user_projects, ServiceResponse, user_id_or_username: &str, pat: Option<&str>],
|
||||
[get_user_projects_deserialized_common, Vec<CommonProject>, user_id_or_username: &str, pat: Option<&str>],
|
||||
[edit_project, ServiceResponse, id_or_slug: &str, patch: serde_json::Value, pat: Option<&str>],
|
||||
[edit_project_bulk, ServiceResponse, ids_or_slugs: &[&str], patch: serde_json::Value, pat: Option<&str>],
|
||||
[edit_project_icon, ServiceResponse, id_or_slug: &str, icon: Option<ImageData>, pat: Option<&str>],
|
||||
[add_gallery_item, ServiceResponse, id_or_slug: &str, image: ImageData, featured: bool, title: Option<String>, description: Option<String>, ordering: Option<i32>, pat: Option<&str>],
|
||||
[remove_gallery_item, ServiceResponse, id_or_slug: &str, image_url: &str, pat: Option<&str>],
|
||||
[edit_gallery_item, ServiceResponse, id_or_slug: &str, image_url: &str, patch: HashMap<String, String>, pat: Option<&str>],
|
||||
[create_report, ServiceResponse, report_type: &str, id: &str, item_type: crate::common::api_common::models::CommonItemType, body: &str, pat: Option<&str>],
|
||||
[get_report, ServiceResponse, id: &str, pat: Option<&str>],
|
||||
[get_reports, ServiceResponse, ids: &[&str], pat: Option<&str>],
|
||||
[get_user_reports, ServiceResponse, pat: Option<&str>],
|
||||
[edit_report, ServiceResponse, id: &str, patch: serde_json::Value, pat: Option<&str>],
|
||||
[delete_report, ServiceResponse, id: &str, pat: Option<&str>],
|
||||
[get_thread, ServiceResponse, id: &str, pat: Option<&str>],
|
||||
[get_threads, ServiceResponse, ids: &[&str], pat: Option<&str>],
|
||||
[write_to_thread, ServiceResponse, id: &str, r#type : &str, message: &str, pat: Option<&str>],
|
||||
[get_moderation_inbox, ServiceResponse, pat: Option<&str>],
|
||||
[read_thread, ServiceResponse, id: &str, pat: Option<&str>],
|
||||
[delete_thread_message, ServiceResponse, id: &str, pat: Option<&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: Option<&str>],
|
||||
[get_team_members_deserialized_common, Vec<crate::common::api_common::models::CommonTeamMember>, team_id: &str, pat: Option<&str>],
|
||||
[get_teams_members, ServiceResponse, ids: &[&str], pat: Option<&str>],
|
||||
[get_project_members, ServiceResponse, id_or_slug: &str, pat: Option<&str>],
|
||||
[get_project_members_deserialized_common, Vec<crate::common::api_common::models::CommonTeamMember>, id_or_slug: &str, pat: Option<&str>],
|
||||
[get_organization_members, ServiceResponse, id_or_title: &str, pat: Option<&str>],
|
||||
[get_organization_members_deserialized_common, Vec<crate::common::api_common::models::CommonTeamMember>, id_or_title: &str, pat: Option<&str>],
|
||||
[join_team, ServiceResponse, team_id: &str, pat: Option<&str>],
|
||||
[remove_from_team, ServiceResponse, team_id: &str, user_id: &str, pat: Option<&str>],
|
||||
[edit_team_member, ServiceResponse, team_id: &str, user_id: &str, patch: serde_json::Value, pat: Option<&str>],
|
||||
[transfer_team_ownership, ServiceResponse, team_id: &str, user_id: &str, pat: Option<&str>],
|
||||
[get_user_notifications, ServiceResponse, user_id: &str, pat: Option<&str>],
|
||||
[get_user_notifications_deserialized_common, Vec<crate::common::api_common::models::CommonNotification>, user_id: &str, pat: Option<&str>],
|
||||
[get_notification, ServiceResponse, notification_id: &str, pat: Option<&str>],
|
||||
[get_notifications, ServiceResponse, ids: &[&str], pat: Option<&str>],
|
||||
[mark_notification_read, ServiceResponse, notification_id: &str, pat: Option<&str>],
|
||||
[mark_notifications_read, ServiceResponse, ids: &[&str], pat: Option<&str>],
|
||||
[add_user_to_team, ServiceResponse, team_id: &str, user_id: &str, project_permissions: Option<ProjectPermissions>, organization_permissions: Option<OrganizationPermissions>, pat: Option<&str>],
|
||||
[delete_notification, ServiceResponse, notification_id: &str, pat: Option<&str>],
|
||||
[delete_notifications, ServiceResponse, ids: &[&str], pat: Option<&str>],
|
||||
}
|
||||
);
|
||||
|
||||
delegate_api_variant!(
|
||||
#[async_trait(?Send)]
|
||||
impl ApiUser for GenericApi {
|
||||
[get_user, ServiceResponse, id_or_username: &str, pat: Option<&str>],
|
||||
[get_current_user, ServiceResponse, pat: Option<&str>],
|
||||
[edit_user, ServiceResponse, id_or_username: &str, patch: serde_json::Value, pat: Option<&str>],
|
||||
[delete_user, ServiceResponse, id_or_username: &str, pat: Option<&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: Option<&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: Option<&str>],
|
||||
[get_version, ServiceResponse, id_or_slug: &str, pat: Option<&str>],
|
||||
[get_version_deserialized_common, CommonVersion, id_or_slug: &str, pat: Option<&str>],
|
||||
[get_versions, ServiceResponse, ids_or_slugs: Vec<String>, pat: Option<&str>],
|
||||
[get_versions_deserialized_common, Vec<CommonVersion>, ids_or_slugs: Vec<String>, pat: Option<&str>],
|
||||
[download_version_redirect, ServiceResponse, hash: &str, algorithm: &str, pat: Option<&str>],
|
||||
[edit_version, ServiceResponse, id_or_slug: &str, patch: serde_json::Value, pat: Option<&str>],
|
||||
[get_version_from_hash, ServiceResponse, id_or_slug: &str, hash: &str, pat: Option<&str>],
|
||||
[get_version_from_hash_deserialized_common, CommonVersion, id_or_slug: &str, hash: &str, pat: Option<&str>],
|
||||
[get_versions_from_hashes, ServiceResponse, hashes: &[&str], algorithm: &str, pat: Option<&str>],
|
||||
[get_versions_from_hashes_deserialized_common, HashMap<String, CommonVersion>, hashes: &[&str], algorithm: &str, pat: Option<&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: Option<&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: Option<&str>],
|
||||
[update_files, ServiceResponse, algorithm: &str, hashes: Vec<String>, loaders: Option<Vec<String>>, game_versions: Option<Vec<String>>, version_types: Option<Vec<String>>, pat: Option<&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: Option<&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: Option<&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: Option<&str>],
|
||||
[edit_version_ordering, ServiceResponse, version_id: &str,ordering: Option<i32>,pat: Option<&str>],
|
||||
[upload_file_to_version, ServiceResponse, version_id: &str, file: &TestFile, pat: Option<&str>],
|
||||
[remove_version, ServiceResponse, version_id: &str, pat: Option<&str>],
|
||||
[remove_version_file, ServiceResponse, hash: &str, pat: Option<&str>],
|
||||
}
|
||||
);
|
||||
403
apps/labrinth/tests/common/api_common/mod.rs
Normal file
403
apps/labrinth/tests/common/api_common/mod.rs
Normal file
@@ -0,0 +1,403 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use self::models::{
|
||||
CommonCategoryData, CommonItemType, CommonLoaderData, CommonNotification, CommonProject,
|
||||
CommonTeamMember, CommonVersion,
|
||||
};
|
||||
use self::request_data::{ImageData, ProjectCreationRequestData};
|
||||
use actix_web::dev::ServiceResponse;
|
||||
use async_trait::async_trait;
|
||||
use labrinth::{
|
||||
models::{
|
||||
projects::{ProjectId, VersionType},
|
||||
teams::{OrganizationPermissions, ProjectPermissions},
|
||||
},
|
||||
LabrinthConfig,
|
||||
};
|
||||
|
||||
use super::dummy_data::TestFile;
|
||||
|
||||
pub mod generic;
|
||||
pub mod models;
|
||||
pub mod request_data;
|
||||
#[async_trait(?Send)]
|
||||
pub trait ApiBuildable: Api {
|
||||
async fn build(labrinth_config: LabrinthConfig) -> Self;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait Api: ApiProject + ApiTags + ApiTeams + ApiUser + 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: Option<&str>,
|
||||
) -> (CommonProject, Vec<CommonVersion>);
|
||||
async fn create_project(
|
||||
&self,
|
||||
creation_data: ProjectCreationRequestData,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn get_public_project_creation_data_json(
|
||||
&self,
|
||||
slug: &str,
|
||||
version_jar: Option<&TestFile>,
|
||||
) -> serde_json::Value;
|
||||
|
||||
async fn remove_project(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_project(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_project_deserialized_common(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
pat: Option<&str>,
|
||||
) -> CommonProject;
|
||||
async fn get_projects(&self, ids_or_slugs: &[&str], pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_project_dependencies(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn get_user_projects(
|
||||
&self,
|
||||
user_id_or_username: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn get_user_projects_deserialized_common(
|
||||
&self,
|
||||
user_id_or_username: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonProject>;
|
||||
async fn edit_project(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn edit_project_bulk(
|
||||
&self,
|
||||
ids_or_slugs: &[&str],
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn edit_project_icon(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
icon: Option<ImageData>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn add_gallery_item(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
image: ImageData,
|
||||
featured: bool,
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
ordering: Option<i32>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn remove_gallery_item(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
url: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn edit_gallery_item(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
url: &str,
|
||||
patch: HashMap<String, String>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn create_report(
|
||||
&self,
|
||||
report_type: &str,
|
||||
id: &str,
|
||||
item_type: CommonItemType,
|
||||
body: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn get_report(&self, id: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_reports(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_user_reports(&self, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn edit_report(
|
||||
&self,
|
||||
id: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn delete_report(&self, id: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_thread(&self, id: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_threads(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse;
|
||||
async fn write_to_thread(
|
||||
&self,
|
||||
id: &str,
|
||||
r#type: &str,
|
||||
message: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn get_moderation_inbox(&self, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn read_thread(&self, id: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn delete_thread_message(&self, id: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
}
|
||||
|
||||
#[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: Option<&str>) -> ServiceResponse;
|
||||
async fn get_team_members_deserialized_common(
|
||||
&self,
|
||||
team_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonTeamMember>;
|
||||
async fn get_teams_members(&self, team_ids: &[&str], pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_project_members(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_project_members_deserialized_common(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonTeamMember>;
|
||||
async fn get_organization_members(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn get_organization_members_deserialized_common(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonTeamMember>;
|
||||
async fn join_team(&self, team_id: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn remove_from_team(
|
||||
&self,
|
||||
team_id: &str,
|
||||
user_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn edit_team_member(
|
||||
&self,
|
||||
team_id: &str,
|
||||
user_id: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn transfer_team_ownership(
|
||||
&self,
|
||||
team_id: &str,
|
||||
user_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn get_user_notifications(&self, user_id: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_user_notifications_deserialized_common(
|
||||
&self,
|
||||
user_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonNotification>;
|
||||
async fn get_notification(&self, notification_id: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_notifications(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse;
|
||||
async fn mark_notification_read(
|
||||
&self,
|
||||
notification_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn mark_notifications_read(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse;
|
||||
async fn add_user_to_team(
|
||||
&self,
|
||||
team_id: &str,
|
||||
user_id: &str,
|
||||
project_permissions: Option<ProjectPermissions>,
|
||||
organization_permissions: Option<OrganizationPermissions>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn delete_notification(
|
||||
&self,
|
||||
notification_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn delete_notifications(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse;
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
pub trait ApiUser {
|
||||
async fn get_user(&self, id_or_username: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_current_user(&self, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn edit_user(
|
||||
&self,
|
||||
id_or_username: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn delete_user(&self, id_or_username: &str, pat: Option<&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: Option<&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: Option<&str>,
|
||||
) -> CommonVersion;
|
||||
async fn get_version(&self, id: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_version_deserialized_common(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
pat: Option<&str>,
|
||||
) -> CommonVersion;
|
||||
async fn get_versions(&self, ids: Vec<String>, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn get_versions_deserialized_common(
|
||||
&self,
|
||||
ids: Vec<String>,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonVersion>;
|
||||
async fn download_version_redirect(
|
||||
&self,
|
||||
hash: &str,
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn edit_version(
|
||||
&self,
|
||||
id: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn get_version_from_hash(
|
||||
&self,
|
||||
hash: &str,
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn get_version_from_hash_deserialized_common(
|
||||
&self,
|
||||
hash: &str,
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> CommonVersion;
|
||||
async fn get_versions_from_hashes(
|
||||
&self,
|
||||
hashes: &[&str],
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn get_versions_from_hashes_deserialized_common(
|
||||
&self,
|
||||
hashes: &[&str],
|
||||
algorithm: &str,
|
||||
pat: Option<&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: Option<&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: Option<&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: Option<&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: Option<&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: Option<&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: Option<&str>,
|
||||
) -> Vec<CommonVersion>;
|
||||
async fn edit_version_ordering(
|
||||
&self,
|
||||
version_id: &str,
|
||||
ordering: Option<i32>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn upload_file_to_version(
|
||||
&self,
|
||||
version_id: &str,
|
||||
file: &TestFile,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse;
|
||||
async fn remove_version(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
async fn remove_version_file(&self, hash: &str, pat: Option<&str>) -> ServiceResponse;
|
||||
}
|
||||
|
||||
pub trait AppendsOptionalPat {
|
||||
fn append_pat(self, pat: Option<&str>) -> Self;
|
||||
}
|
||||
// Impl this on all actix_web::test::TestRequest
|
||||
impl AppendsOptionalPat for actix_web::test::TestRequest {
|
||||
fn append_pat(self, pat: Option<&str>) -> Self {
|
||||
if let Some(pat) = pat {
|
||||
self.append_header(("Authorization", pat))
|
||||
} else {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
231
apps/labrinth/tests/common/api_common/models.rs
Normal file
231
apps/labrinth/tests/common/api_common/models.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use labrinth::{
|
||||
auth::AuthProvider,
|
||||
models::{
|
||||
images::ImageId,
|
||||
notifications::NotificationId,
|
||||
organizations::OrganizationId,
|
||||
projects::{
|
||||
Dependency, GalleryItem, License, ModeratorMessage, MonetizationStatus, ProjectId,
|
||||
ProjectStatus, VersionFile, VersionId, VersionStatus, VersionType,
|
||||
},
|
||||
reports::ReportId,
|
||||
teams::{ProjectPermissions, TeamId},
|
||||
threads::{ThreadId, ThreadMessageId},
|
||||
users::{Badges, Role, User, UserId, UserPayoutData},
|
||||
},
|
||||
};
|
||||
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.
|
||||
|
||||
// Exceptions are fields that *should* be changing across the API, and older versions
|
||||
// should be unsupported on API version increase- for example, payouts related financial fields.
|
||||
|
||||
// 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 organization: Option<OrganizationId>,
|
||||
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 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 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>,
|
||||
}
|
||||
|
||||
#[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,
|
||||
|
||||
pub permissions: Option<ProjectPermissions>,
|
||||
|
||||
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>,
|
||||
// Body is absent as one of the variants differs
|
||||
pub text: String,
|
||||
pub link: String,
|
||||
pub actions: Vec<CommonNotificationAction>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CommonNotificationAction {
|
||||
pub action_route: (String, String),
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum CommonItemType {
|
||||
Project,
|
||||
Version,
|
||||
User,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl CommonItemType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
CommonItemType::Project => "project",
|
||||
CommonItemType::Version => "version",
|
||||
CommonItemType::User => "user",
|
||||
CommonItemType::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CommonReport {
|
||||
pub id: ReportId,
|
||||
pub report_type: String,
|
||||
pub item_id: String,
|
||||
pub item_type: CommonItemType,
|
||||
pub reporter: UserId,
|
||||
pub body: String,
|
||||
pub created: DateTime<Utc>,
|
||||
pub closed: bool,
|
||||
pub thread_id: ThreadId,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub enum LegacyItemType {
|
||||
Project,
|
||||
Version,
|
||||
User,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CommonThread {
|
||||
pub id: ThreadId,
|
||||
#[serde(rename = "type")]
|
||||
pub type_: CommonThreadType,
|
||||
pub project_id: Option<ProjectId>,
|
||||
pub report_id: Option<ReportId>,
|
||||
pub messages: Vec<CommonThreadMessage>,
|
||||
pub members: Vec<User>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CommonThreadMessage {
|
||||
pub id: ThreadMessageId,
|
||||
pub author_id: Option<UserId>,
|
||||
pub body: CommonMessageBody,
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub enum CommonMessageBody {
|
||||
Text {
|
||||
body: String,
|
||||
#[serde(default)]
|
||||
private: bool,
|
||||
replying_to: Option<ThreadMessageId>,
|
||||
#[serde(default)]
|
||||
associated_images: Vec<ImageId>,
|
||||
},
|
||||
StatusChange {
|
||||
new_status: ProjectStatus,
|
||||
old_status: ProjectStatus,
|
||||
},
|
||||
ThreadClosure,
|
||||
ThreadReopen,
|
||||
Deleted,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub enum CommonThreadType {
|
||||
Report,
|
||||
Project,
|
||||
DirectMessage,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CommonUser {
|
||||
pub id: UserId,
|
||||
pub username: String,
|
||||
pub name: Option<String>,
|
||||
pub avatar_url: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub created: DateTime<Utc>,
|
||||
pub role: Role,
|
||||
pub badges: Badges,
|
||||
pub auth_providers: Option<Vec<AuthProvider>>,
|
||||
pub email: Option<String>,
|
||||
pub email_verified: Option<bool>,
|
||||
pub has_password: Option<bool>,
|
||||
pub has_totp: Option<bool>,
|
||||
pub payout_data: Option<UserPayoutData>,
|
||||
pub github_id: Option<u64>,
|
||||
}
|
||||
24
apps/labrinth/tests/common/api_common/request_data.rs
Normal file
24
apps/labrinth/tests/common/api_common/request_data.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
// The structures for project/version creation.
|
||||
// These are created differently, but are essentially the same between versions.
|
||||
|
||||
use labrinth::util::actix::MultipartSegment;
|
||||
|
||||
use crate::common::dummy_data::TestFile;
|
||||
|
||||
pub struct ProjectCreationRequestData {
|
||||
pub slug: String,
|
||||
pub jar: Option<TestFile>,
|
||||
pub segment_data: Vec<MultipartSegment>,
|
||||
}
|
||||
|
||||
pub struct VersionCreationRequestData {
|
||||
pub version: String,
|
||||
pub jar: Option<TestFile>,
|
||||
pub segment_data: Vec<MultipartSegment>,
|
||||
}
|
||||
|
||||
pub struct ImageData {
|
||||
pub filename: String,
|
||||
pub extension: String,
|
||||
pub icon: Vec<u8>,
|
||||
}
|
||||
50
apps/labrinth/tests/common/api_v2/mod.rs
Normal file
50
apps/labrinth/tests/common/api_v2/mod.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
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;
|
||||
pub mod request_data;
|
||||
pub mod tags;
|
||||
pub mod team;
|
||||
pub mod user;
|
||||
pub mod version;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ApiV2 {
|
||||
pub test_app: Rc<dyn LocalService>,
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
|
||||
async fn reset_search_index(&self) -> ServiceResponse {
|
||||
let req = actix_web::test::TestRequest::post()
|
||||
.uri("/v2/admin/_force_reindex")
|
||||
.append_header((
|
||||
"Modrinth-Admin",
|
||||
dotenvy::var("LABRINTH_ADMIN_KEY").unwrap(),
|
||||
))
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
}
|
||||
521
apps/labrinth/tests/common/api_v2/project.rs
Normal file
521
apps/labrinth/tests/common/api_v2/project.rs
Normal file
@@ -0,0 +1,521 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::{
|
||||
api_common::{
|
||||
models::{CommonItemType, CommonProject, CommonVersion},
|
||||
request_data::{ImageData, ProjectCreationRequestData},
|
||||
Api, ApiProject, AppendsOptionalPat,
|
||||
},
|
||||
dummy_data::TestFile,
|
||||
},
|
||||
};
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{
|
||||
dev::ServiceResponse,
|
||||
test::{self, TestRequest},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
use labrinth::{
|
||||
models::v2::{projects::LegacyProject, search::LegacySearchResults},
|
||||
util::actix::AppendsMultipart,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::common::database::MOD_USER_PAT;
|
||||
|
||||
use super::{
|
||||
request_data::{self, get_public_project_creation_data},
|
||||
ApiV2,
|
||||
};
|
||||
|
||||
impl ApiV2 {
|
||||
pub async fn get_project_deserialized(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
pat: Option<&str>,
|
||||
) -> LegacyProject {
|
||||
let resp = self.get_project(id_or_slug, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_user_projects_deserialized(
|
||||
&self,
|
||||
user_id_or_username: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<LegacyProject> {
|
||||
let resp = self.get_user_projects(user_id_or_username, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn search_deserialized(
|
||||
&self,
|
||||
query: Option<&str>,
|
||||
facets: Option<serde_json::Value>,
|
||||
pat: Option<&str>,
|
||||
) -> LegacySearchResults {
|
||||
let query_field = if let Some(query) = query {
|
||||
format!("&query={}", urlencoding::encode(query))
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let facets_field = if let Some(facets) = facets {
|
||||
format!("&facets={}", urlencoding::encode(&facets.to_string()))
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/search?{}{}", query_field, facets_field))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
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: Option<&str>,
|
||||
) -> (CommonProject, Vec<CommonVersion>) {
|
||||
let creation_data = get_public_project_creation_data(slug, version_jar, modify_json);
|
||||
|
||||
// Add a project.
|
||||
let slug = creation_data.slug.clone();
|
||||
let resp = self.create_project(creation_data, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
// Approve as a moderator.
|
||||
let req = TestRequest::patch()
|
||||
.uri(&format!("/v2/project/{}", slug))
|
||||
.append_pat(MOD_USER_PAT)
|
||||
.set_json(json!(
|
||||
{
|
||||
"status": "approved"
|
||||
}
|
||||
))
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let project = self.get_project_deserialized_common(&slug, pat).await;
|
||||
|
||||
// Get project's versions
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v2/project/{}/version", slug))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
let versions: Vec<CommonVersion> = test::read_body_json(resp).await;
|
||||
|
||||
(project, versions)
|
||||
}
|
||||
|
||||
async fn get_public_project_creation_data_json(
|
||||
&self,
|
||||
slug: &str,
|
||||
version_jar: Option<&TestFile>,
|
||||
) -> serde_json::Value {
|
||||
request_data::get_public_project_creation_data_json(slug, version_jar)
|
||||
}
|
||||
|
||||
async fn create_project(
|
||||
&self,
|
||||
creation_data: ProjectCreationRequestData,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::post()
|
||||
.uri("/v2/project")
|
||||
.append_pat(pat)
|
||||
.set_multipart(creation_data.segment_data)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn remove_project(&self, project_slug_or_id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v2/project/{project_slug_or_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_project(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v2/project/{id_or_slug}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_project_deserialized_common(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
pat: Option<&str>,
|
||||
) -> CommonProject {
|
||||
let resp = self.get_project(id_or_slug, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let project: LegacyProject = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(project).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_projects(&self, ids_or_slugs: &[&str], pat: Option<&str>) -> ServiceResponse {
|
||||
let ids_or_slugs = serde_json::to_string(ids_or_slugs).unwrap();
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v2/projects?ids={encoded}",
|
||||
encoded = urlencoding::encode(&ids_or_slugs)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_project_dependencies(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v2/project/{id_or_slug}/dependencies"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_user_projects(
|
||||
&self,
|
||||
user_id_or_username: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/user/{}/projects", user_id_or_username))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_user_projects_deserialized_common(
|
||||
&self,
|
||||
user_id_or_username: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonProject> {
|
||||
let resp = self.get_user_projects(user_id_or_username, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let projects: Vec<LegacyProject> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(projects).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn edit_project(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v2/project/{id_or_slug}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn edit_project_bulk(
|
||||
&self,
|
||||
ids_or_slugs: &[&str],
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let projects_str = ids_or_slugs
|
||||
.iter()
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!(
|
||||
"/v2/projects?ids={encoded}",
|
||||
encoded = urlencoding::encode(&format!("[{projects_str}]"))
|
||||
))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn edit_project_icon(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
icon: Option<ImageData>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
if let Some(icon) = icon {
|
||||
// If an icon is provided, upload it
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!(
|
||||
"/v2/project/{id_or_slug}/icon?ext={ext}",
|
||||
ext = icon.extension
|
||||
))
|
||||
.append_pat(pat)
|
||||
.set_payload(Bytes::from(icon.icon))
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
} else {
|
||||
// If no icon is provided, delete the icon
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v2/project/{id_or_slug}/icon"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_report(
|
||||
&self,
|
||||
report_type: &str,
|
||||
id: &str,
|
||||
item_type: CommonItemType,
|
||||
body: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/v2/report")
|
||||
.append_pat(pat)
|
||||
.set_json(json!(
|
||||
{
|
||||
"report_type": report_type,
|
||||
"item_id": id,
|
||||
"item_type": item_type.as_str(),
|
||||
"body": body,
|
||||
}
|
||||
))
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_report(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/report/{id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_reports(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse {
|
||||
let ids_str = serde_json::to_string(ids).unwrap();
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v2/reports?ids={encoded}",
|
||||
encoded = urlencoding::encode(&ids_str)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_user_reports(&self, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/v2/report")
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn delete_report(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v2/report/{id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn edit_report(
|
||||
&self,
|
||||
id: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v2/report/{id}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_thread(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/thread/{id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_threads(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse {
|
||||
let ids_str = serde_json::to_string(ids).unwrap();
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v3/threads?ids={encoded}",
|
||||
encoded = urlencoding::encode(&ids_str)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn write_to_thread(
|
||||
&self,
|
||||
id: &str,
|
||||
r#type: &str,
|
||||
content: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&format!("/v2/thread/{id}"))
|
||||
.append_pat(pat)
|
||||
.set_json(json!({
|
||||
"body" : {
|
||||
"type": r#type,
|
||||
"body": content,
|
||||
}
|
||||
}))
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_moderation_inbox(&self, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/v2/thread/inbox")
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn read_thread(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&format!("/v2/thread/{id}/read"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn delete_thread_message(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v2/message/{id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn add_gallery_item(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
image: ImageData,
|
||||
featured: bool,
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
ordering: Option<i32>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let mut url = format!(
|
||||
"/v2/project/{id_or_slug}/gallery?ext={ext}&featured={featured}",
|
||||
ext = image.extension,
|
||||
featured = featured
|
||||
);
|
||||
if let Some(title) = title {
|
||||
url.push_str(&format!("&title={}", title));
|
||||
}
|
||||
if let Some(description) = description {
|
||||
url.push_str(&format!("&description={}", description));
|
||||
}
|
||||
if let Some(ordering) = ordering {
|
||||
url.push_str(&format!("&ordering={}", ordering));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&url)
|
||||
.append_pat(pat)
|
||||
.set_payload(Bytes::from(image.icon))
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn edit_gallery_item(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
image_url: &str,
|
||||
patch: HashMap<String, String>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let mut url = format!(
|
||||
"/v2/project/{id_or_slug}/gallery?url={image_url}",
|
||||
image_url = urlencoding::encode(image_url)
|
||||
);
|
||||
|
||||
for (key, value) in patch {
|
||||
url.push_str(&format!(
|
||||
"&{key}={value}",
|
||||
key = key,
|
||||
value = urlencoding::encode(&value)
|
||||
));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&url)
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn remove_gallery_item(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
url: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!(
|
||||
"/v2/project/{id_or_slug}/gallery?url={url}",
|
||||
url = url
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
}
|
||||
125
apps/labrinth/tests/common/api_v2/request_data.rs
Normal file
125
apps/labrinth/tests/common/api_v2/request_data.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
#![allow(dead_code)]
|
||||
use serde_json::json;
|
||||
|
||||
use crate::common::{
|
||||
api_common::request_data::{ProjectCreationRequestData, VersionCreationRequestData},
|
||||
dummy_data::TestFile,
|
||||
};
|
||||
use labrinth::{
|
||||
models::projects::ProjectId,
|
||||
util::actix::{MultipartSegment, MultipartSegmentData},
|
||||
};
|
||||
|
||||
pub fn get_public_project_creation_data(
|
||||
slug: &str,
|
||||
version_jar: Option<TestFile>,
|
||||
modify_json: Option<json_patch::Patch>,
|
||||
) -> ProjectCreationRequestData {
|
||||
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(),
|
||||
jar: version_jar,
|
||||
segment_data: multipart_data,
|
||||
}
|
||||
}
|
||||
|
||||
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, 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(),
|
||||
jar: Some(version_jar),
|
||||
segment_data: multipart_data,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_public_version_creation_data_json(
|
||||
version_number: &str,
|
||||
ordering: Option<i32>,
|
||||
version_jar: &TestFile,
|
||||
) -> serde_json::Value {
|
||||
let mut j = json!({
|
||||
"file_parts": [version_jar.filename()],
|
||||
"version_number": version_number,
|
||||
"version_title": "start",
|
||||
"dependencies": [],
|
||||
"game_versions": ["1.20.1"] ,
|
||||
"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(
|
||||
slug: &str,
|
||||
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", None, jar)])
|
||||
} else {
|
||||
json!([])
|
||||
};
|
||||
|
||||
let is_draft = version_jar.is_none();
|
||||
json!(
|
||||
{
|
||||
"title": format!("Test Project {slug}"),
|
||||
"slug": slug,
|
||||
"project_type": version_jar.as_ref().map(|f| f.project_type()).unwrap_or("mod".to_string()),
|
||||
"description": "A dummy project for testing with.",
|
||||
"body": "This project is approved, and versions are listed.",
|
||||
"client_side": "required",
|
||||
"server_side": "optional",
|
||||
"initial_versions": initial_versions,
|
||||
"is_draft": is_draft,
|
||||
"categories": [],
|
||||
"license_id": "MIT",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_public_creation_data_multipart(
|
||||
json_data: &serde_json::Value,
|
||||
version_jar: Option<&TestFile>,
|
||||
) -> Vec<MultipartSegment> {
|
||||
// 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()),
|
||||
};
|
||||
|
||||
if let Some(jar) = version_jar {
|
||||
// Basic file
|
||||
let file_segment = MultipartSegment {
|
||||
name: jar.filename(),
|
||||
filename: Some(jar.filename()),
|
||||
content_type: Some("application/java-archive".to_string()),
|
||||
data: MultipartSegmentData::Binary(jar.bytes()),
|
||||
};
|
||||
|
||||
vec![json_segment, file_segment]
|
||||
} else {
|
||||
vec![json_segment]
|
||||
}
|
||||
}
|
||||
117
apps/labrinth/tests/common/api_v2/tags.rs
Normal file
117
apps/labrinth/tests/common/api_v2/tags.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{
|
||||
dev::ServiceResponse,
|
||||
test::{self, TestRequest},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use labrinth::routes::v2::tags::{
|
||||
CategoryData, DonationPlatformQueryData, GameVersionQueryData, LoaderData,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::{
|
||||
api_common::{
|
||||
models::{CommonCategoryData, CommonLoaderData},
|
||||
Api, ApiTags, AppendsOptionalPat,
|
||||
},
|
||||
database::ADMIN_USER_PAT,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ApiV2;
|
||||
|
||||
impl ApiV2 {
|
||||
async fn get_side_types(&self) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri("/v2/tag/side_type")
|
||||
.append_pat(ADMIN_USER_PAT)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_side_types_deserialized(&self) -> Vec<String> {
|
||||
let resp = self.get_side_types().await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_game_versions(&self) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri("/v2/tag/game_version")
|
||||
.append_pat(ADMIN_USER_PAT)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_game_versions_deserialized(&self) -> Vec<GameVersionQueryData> {
|
||||
let resp = self.get_game_versions().await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_loaders_deserialized(&self) -> Vec<LoaderData> {
|
||||
let resp = self.get_loaders().await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_categories_deserialized(&self) -> Vec<CategoryData> {
|
||||
let resp = self.get_categories().await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_donation_platforms(&self) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri("/v2/tag/donation_platform")
|
||||
.append_pat(ADMIN_USER_PAT)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_donation_platforms_deserialized(&self) -> Vec<DonationPlatformQueryData> {
|
||||
let resp = self.get_donation_platforms().await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
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_pat(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_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<LoaderData> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_categories(&self) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri("/v2/tag/category")
|
||||
.append_pat(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_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<CategoryData> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
}
|
||||
313
apps/labrinth/tests/common/api_v2/team.rs
Normal file
313
apps/labrinth/tests/common/api_v2/team.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{dev::ServiceResponse, test};
|
||||
use async_trait::async_trait;
|
||||
use labrinth::models::{
|
||||
teams::{OrganizationPermissions, ProjectPermissions},
|
||||
v2::{notifications::LegacyNotification, teams::LegacyTeamMember},
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::api_common::{
|
||||
models::{CommonNotification, CommonTeamMember},
|
||||
Api, ApiTeams, AppendsOptionalPat,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ApiV2;
|
||||
|
||||
impl ApiV2 {
|
||||
pub async fn get_organization_members_deserialized(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<LegacyTeamMember> {
|
||||
let resp = self.get_organization_members(id_or_title, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_team_members_deserialized(
|
||||
&self,
|
||||
team_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<LegacyTeamMember> {
|
||||
let resp = self.get_team_members(team_id, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_user_notifications_deserialized(
|
||||
&self,
|
||||
user_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<LegacyNotification> {
|
||||
let resp = self.get_user_notifications(user_id, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl ApiTeams for ApiV2 {
|
||||
async fn get_team_members(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/team/{id_or_title}/members"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_team_members_deserialized_common(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonTeamMember> {
|
||||
let resp = self.get_team_members(id_or_title, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<LegacyTeamMember> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_teams_members(
|
||||
&self,
|
||||
ids_or_titles: &[&str],
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let ids_or_titles = serde_json::to_string(ids_or_titles).unwrap();
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v2/teams?ids={}",
|
||||
urlencoding::encode(&ids_or_titles)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_project_members(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/project/{id_or_title}/members"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_project_members_deserialized_common(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonTeamMember> {
|
||||
let resp = self.get_project_members(id_or_title, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<LegacyTeamMember> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_organization_members(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/organization/{id_or_title}/members"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_organization_members_deserialized_common(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonTeamMember> {
|
||||
let resp = self.get_organization_members(id_or_title, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<LegacyTeamMember> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn join_team(&self, team_id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&format!("/v2/team/{team_id}/join"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn remove_from_team(
|
||||
&self,
|
||||
team_id: &str,
|
||||
user_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v2/team/{team_id}/members/{user_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn edit_team_member(
|
||||
&self,
|
||||
team_id: &str,
|
||||
user_id: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v2/team/{team_id}/members/{user_id}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn transfer_team_ownership(
|
||||
&self,
|
||||
team_id: &str,
|
||||
user_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v2/team/{team_id}/owner"))
|
||||
.append_pat(pat)
|
||||
.set_json(json!({
|
||||
"user_id": user_id,
|
||||
}))
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_user_notifications(&self, user_id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/user/{user_id}/notifications"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_user_notifications_deserialized_common(
|
||||
&self,
|
||||
user_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonNotification> {
|
||||
let resp = self.get_user_notifications(user_id, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<LegacyNotification> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_notification(&self, notification_id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/notification/{notification_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_notifications(
|
||||
&self,
|
||||
notification_ids: &[&str],
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let notification_ids = serde_json::to_string(notification_ids).unwrap();
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v2/notifications?ids={}",
|
||||
urlencoding::encode(¬ification_ids)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn mark_notification_read(
|
||||
&self,
|
||||
notification_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v2/notification/{notification_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn mark_notifications_read(
|
||||
&self,
|
||||
notification_ids: &[&str],
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let notification_ids = serde_json::to_string(notification_ids).unwrap();
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!(
|
||||
"/v2/notifications?ids={}",
|
||||
urlencoding::encode(¬ification_ids)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn add_user_to_team(
|
||||
&self,
|
||||
team_id: &str,
|
||||
user_id: &str,
|
||||
project_permissions: Option<ProjectPermissions>,
|
||||
organization_permissions: Option<OrganizationPermissions>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&format!("/v2/team/{team_id}/members"))
|
||||
.append_pat(pat)
|
||||
.set_json(json!( {
|
||||
"user_id": user_id,
|
||||
"permissions" : project_permissions.map(|p| p.bits()).unwrap_or_default(),
|
||||
"organization_permissions" : organization_permissions.map(|p| p.bits()),
|
||||
}))
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn delete_notification(
|
||||
&self,
|
||||
notification_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v2/notification/{notification_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn delete_notifications(
|
||||
&self,
|
||||
notification_ids: &[&str],
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let notification_ids = serde_json::to_string(notification_ids).unwrap();
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!(
|
||||
"/v2/notifications?ids={}",
|
||||
urlencoding::encode(¬ification_ids)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
}
|
||||
47
apps/labrinth/tests/common/api_v2/user.rs
Normal file
47
apps/labrinth/tests/common/api_v2/user.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
use super::ApiV2;
|
||||
use crate::common::api_common::{Api, ApiUser, AppendsOptionalPat};
|
||||
use actix_web::{dev::ServiceResponse, test};
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl ApiUser for ApiV2 {
|
||||
async fn get_user(&self, user_id_or_username: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/user/{}", user_id_or_username))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_current_user(&self, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/v2/user")
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn edit_user(
|
||||
&self,
|
||||
user_id_or_username: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v2/user/{}", user_id_or_username))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn delete_user(&self, user_id_or_username: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v2/user/{}", user_id_or_username))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
}
|
||||
509
apps/labrinth/tests/common/api_v2/version.rs
Normal file
509
apps/labrinth/tests/common/api_v2/version.rs
Normal file
@@ -0,0 +1,509 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{
|
||||
request_data::{self, get_public_version_creation_data},
|
||||
ApiV2,
|
||||
};
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::{
|
||||
api_common::{models::CommonVersion, Api, ApiVersion, AppendsOptionalPat},
|
||||
dummy_data::TestFile,
|
||||
},
|
||||
};
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{
|
||||
dev::ServiceResponse,
|
||||
test::{self, TestRequest},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use labrinth::{
|
||||
models::{
|
||||
projects::{ProjectId, VersionType},
|
||||
v2::projects::LegacyVersion,
|
||||
},
|
||||
routes::v2::version_file::FileUpdateData,
|
||||
util::actix::AppendsMultipart,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
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 get_version_deserialized(&self, id: &str, pat: Option<&str>) -> LegacyVersion {
|
||||
let resp = self.get_version(id, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_version_from_hash_deserialized(
|
||||
&self,
|
||||
hash: &str,
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> LegacyVersion {
|
||||
let resp = self.get_version_from_hash(hash, algorithm, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_versions_from_hashes_deserialized(
|
||||
&self,
|
||||
hashes: &[&str],
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> HashMap<String, LegacyVersion> {
|
||||
let resp = self.get_versions_from_hashes(hashes, algorithm, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn update_individual_files(
|
||||
&self,
|
||||
algorithm: &str,
|
||||
hashes: Vec<FileUpdateData>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/v2/version_files/update_individual")
|
||||
.append_pat(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: Option<&str>,
|
||||
) -> HashMap<String, LegacyVersion> {
|
||||
let resp = self.update_individual_files(algorithm, hashes, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
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: Option<&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_pat(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: Option<&str>,
|
||||
) -> CommonVersion {
|
||||
let resp = self
|
||||
.add_public_version(
|
||||
project_id,
|
||||
version_number,
|
||||
version_jar,
|
||||
ordering,
|
||||
modify_json,
|
||||
pat,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: LegacyVersion = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_version(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v2/version/{id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_version_deserialized_common(&self, id: &str, pat: Option<&str>) -> CommonVersion {
|
||||
let resp = self.get_version(id, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: LegacyVersion = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn download_version_redirect(
|
||||
&self,
|
||||
hash: &str,
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/version_file/{hash}/download",))
|
||||
.set_json(json!({
|
||||
"algorithm": algorithm,
|
||||
}))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn edit_version(
|
||||
&self,
|
||||
version_id: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v2/version/{version_id}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_version_from_hash(
|
||||
&self,
|
||||
hash: &str,
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v2/version_file/{hash}?algorithm={algorithm}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_version_from_hash_deserialized_common(
|
||||
&self,
|
||||
hash: &str,
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> CommonVersion {
|
||||
let resp = self.get_version_from_hash(hash, algorithm, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: LegacyVersion = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_versions_from_hashes(
|
||||
&self,
|
||||
hashes: &[&str],
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::post()
|
||||
.uri("/v2/version_files")
|
||||
.append_pat(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: Option<&str>,
|
||||
) -> HashMap<String, CommonVersion> {
|
||||
let resp = self.get_versions_from_hashes(hashes, algorithm, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: HashMap<String, LegacyVersion> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
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: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&format!(
|
||||
"/v2/version_file/{hash}/update?algorithm={algorithm}"
|
||||
))
|
||||
.append_pat(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: Option<&str>,
|
||||
) -> CommonVersion {
|
||||
let resp = self
|
||||
.get_update_from_hash(hash, algorithm, loaders, game_versions, version_types, pat)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: LegacyVersion = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
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: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/v2/version_files/update")
|
||||
.append_pat(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: Option<&str>,
|
||||
) -> HashMap<String, CommonVersion> {
|
||||
let resp = self
|
||||
.update_files(
|
||||
algorithm,
|
||||
hashes,
|
||||
loaders,
|
||||
game_versions,
|
||||
version_types,
|
||||
pat,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: HashMap<String, LegacyVersion> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
// TODO: Not all fields are tested currently in the V2 tests, only the v2-v3 relevant ones are
|
||||
#[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: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let mut query_string = String::new();
|
||||
if let Some(game_versions) = game_versions {
|
||||
query_string.push_str(&format!(
|
||||
"&game_versions={}",
|
||||
urlencoding::encode(&serde_json::to_string(&game_versions).unwrap())
|
||||
));
|
||||
}
|
||||
if let Some(loaders) = loaders {
|
||||
query_string.push_str(&format!(
|
||||
"&loaders={}",
|
||||
urlencoding::encode(&serde_json::to_string(&loaders).unwrap())
|
||||
));
|
||||
}
|
||||
if let Some(featured) = featured {
|
||||
query_string.push_str(&format!("&featured={}", featured));
|
||||
}
|
||||
if let Some(version_type) = version_type {
|
||||
query_string.push_str(&format!("&version_type={}", version_type));
|
||||
}
|
||||
if let Some(limit) = limit {
|
||||
let limit = limit.to_string();
|
||||
query_string.push_str(&format!("&limit={}", limit));
|
||||
}
|
||||
if let Some(offset) = offset {
|
||||
let offset = offset.to_string();
|
||||
query_string.push_str(&format!("&offset={}", offset));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v2/project/{project_id_slug}/version?{}",
|
||||
query_string.trim_start_matches('&')
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
#[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: Option<&str>,
|
||||
) -> Vec<CommonVersion> {
|
||||
let resp = self
|
||||
.get_project_versions(
|
||||
slug,
|
||||
game_versions,
|
||||
loaders,
|
||||
featured,
|
||||
version_type,
|
||||
limit,
|
||||
offset,
|
||||
pat,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<LegacyVersion> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn edit_version_ordering(
|
||||
&self,
|
||||
version_id: &str,
|
||||
ordering: Option<i32>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let request = test::TestRequest::patch()
|
||||
.uri(&format!("/v2/version/{version_id}"))
|
||||
.set_json(json!(
|
||||
{
|
||||
"ordering": ordering
|
||||
}
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
}
|
||||
|
||||
async fn get_versions(&self, version_ids: Vec<String>, pat: Option<&str>) -> ServiceResponse {
|
||||
let ids = url_encode_json_serialized_vec(&version_ids);
|
||||
let request = test::TestRequest::get()
|
||||
.uri(&format!("/v2/versions?ids={}", ids))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
}
|
||||
|
||||
async fn get_versions_deserialized_common(
|
||||
&self,
|
||||
version_ids: Vec<String>,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonVersion> {
|
||||
let resp = self.get_versions(version_ids, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<LegacyVersion> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn upload_file_to_version(
|
||||
&self,
|
||||
version_id: &str,
|
||||
file: &TestFile,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let m = request_data::get_public_creation_data_multipart(
|
||||
&json!({
|
||||
"file_parts": [file.filename()]
|
||||
}),
|
||||
Some(file),
|
||||
);
|
||||
let request = test::TestRequest::post()
|
||||
.uri(&format!("/v2/version/{version_id}/file"))
|
||||
.append_pat(pat)
|
||||
.set_multipart(m)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
}
|
||||
|
||||
async fn remove_version(&self, version_id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let request = test::TestRequest::delete()
|
||||
.uri(&format!("/v2/version/{version_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
}
|
||||
|
||||
async fn remove_version_file(&self, hash: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let request = test::TestRequest::delete()
|
||||
.uri(&format!("/v2/version_file/{hash}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
}
|
||||
}
|
||||
159
apps/labrinth/tests/common/api_v3/collections.rs
Normal file
159
apps/labrinth/tests/common/api_v3/collections.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{
|
||||
dev::ServiceResponse,
|
||||
test::{self, TestRequest},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use labrinth::models::{collections::Collection, v3::projects::Project};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::api_common::{request_data::ImageData, Api, AppendsOptionalPat},
|
||||
};
|
||||
|
||||
use super::ApiV3;
|
||||
|
||||
impl ApiV3 {
|
||||
pub async fn create_collection(
|
||||
&self,
|
||||
collection_title: &str,
|
||||
description: &str,
|
||||
projects: &[&str],
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/v3/collection")
|
||||
.append_pat(pat)
|
||||
.set_json(json!({
|
||||
"name": collection_title,
|
||||
"description": description,
|
||||
"projects": projects,
|
||||
}))
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_collection(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/collection/{id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_collection_deserialized(&self, id: &str, pat: Option<&str>) -> Collection {
|
||||
let resp = self.get_collection(id, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_collections(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse {
|
||||
let ids = serde_json::to_string(ids).unwrap();
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v3/collections?ids={}",
|
||||
urlencoding::encode(&ids)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_collection_projects(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/collection/{id}/projects"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_collection_projects_deserialized(
|
||||
&self,
|
||||
id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<Project> {
|
||||
let resp = self.get_collection_projects(id, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn edit_collection(
|
||||
&self,
|
||||
id: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v3/collection/{id}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn edit_collection_icon(
|
||||
&self,
|
||||
id: &str,
|
||||
icon: Option<ImageData>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
if let Some(icon) = icon {
|
||||
// If an icon is provided, upload it
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!(
|
||||
"/v3/collection/{id}/icon?ext={ext}",
|
||||
ext = icon.extension
|
||||
))
|
||||
.append_pat(pat)
|
||||
.set_payload(Bytes::from(icon.icon))
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
} else {
|
||||
// If no icon is provided, delete the icon
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/collection/{id}/icon"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_collection(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/collection/{id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_user_collections(
|
||||
&self,
|
||||
user_id_or_username: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/user/{}/collections", user_id_or_username))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_user_collections_deserialized_common(
|
||||
&self,
|
||||
user_id_or_username: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<Collection> {
|
||||
let resp = self.get_user_collections(user_id_or_username, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let projects: Vec<Project> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(projects).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
}
|
||||
54
apps/labrinth/tests/common/api_v3/mod.rs
Normal file
54
apps/labrinth/tests/common/api_v3/mod.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
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 collections;
|
||||
pub mod oauth;
|
||||
pub mod oauth_clients;
|
||||
pub mod organization;
|
||||
pub mod project;
|
||||
pub mod request_data;
|
||||
pub mod tags;
|
||||
pub mod team;
|
||||
pub mod user;
|
||||
pub mod version;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ApiV3 {
|
||||
pub test_app: Rc<dyn LocalService>,
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
|
||||
async fn reset_search_index(&self) -> ServiceResponse {
|
||||
let req = actix_web::test::TestRequest::post()
|
||||
.uri("/_internal/admin/_force_reindex")
|
||||
.append_header((
|
||||
"Modrinth-Admin",
|
||||
dotenvy::var("LABRINTH_ADMIN_KEY").unwrap(),
|
||||
))
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
}
|
||||
161
apps/labrinth/tests/common/api_v3/oauth.rs
Normal file
161
apps/labrinth/tests/common/api_v3/oauth.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{
|
||||
dev::ServiceResponse,
|
||||
test::{self, TestRequest},
|
||||
};
|
||||
use labrinth::auth::oauth::{
|
||||
OAuthClientAccessRequest, RespondToOAuthClientScopes, TokenRequest, TokenResponse,
|
||||
};
|
||||
use reqwest::header::{AUTHORIZATION, LOCATION};
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::api_common::{Api, AppendsOptionalPat},
|
||||
};
|
||||
|
||||
use super::ApiV3;
|
||||
|
||||
impl ApiV3 {
|
||||
pub async fn complete_full_authorize_flow(
|
||||
&self,
|
||||
client_id: &str,
|
||||
client_secret: &str,
|
||||
scope: Option<&str>,
|
||||
redirect_uri: Option<&str>,
|
||||
state: Option<&str>,
|
||||
user_pat: Option<&str>,
|
||||
) -> String {
|
||||
let auth_resp = self
|
||||
.oauth_authorize(client_id, scope, redirect_uri, state, user_pat)
|
||||
.await;
|
||||
let flow_id = get_authorize_accept_flow_id(auth_resp).await;
|
||||
let redirect_resp = self.oauth_accept(&flow_id, user_pat).await;
|
||||
let auth_code = get_auth_code_from_redirect_params(&redirect_resp).await;
|
||||
let token_resp = self
|
||||
.oauth_token(auth_code, None, client_id.to_string(), client_secret)
|
||||
.await;
|
||||
get_access_token(token_resp).await
|
||||
}
|
||||
|
||||
pub async fn oauth_authorize(
|
||||
&self,
|
||||
client_id: &str,
|
||||
scope: Option<&str>,
|
||||
redirect_uri: Option<&str>,
|
||||
state: Option<&str>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let uri = generate_authorize_uri(client_id, scope, redirect_uri, state);
|
||||
let req = TestRequest::get().uri(&uri).append_pat(pat).to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn oauth_accept(&self, flow: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
self.call(
|
||||
TestRequest::post()
|
||||
.uri("/_internal/oauth/accept")
|
||||
.append_pat(pat)
|
||||
.set_json(RespondToOAuthClientScopes {
|
||||
flow: flow.to_string(),
|
||||
})
|
||||
.to_request(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn oauth_reject(&self, flow: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
self.call(
|
||||
TestRequest::post()
|
||||
.uri("/_internal/oauth/reject")
|
||||
.append_pat(pat)
|
||||
.set_json(RespondToOAuthClientScopes {
|
||||
flow: flow.to_string(),
|
||||
})
|
||||
.to_request(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn oauth_token(
|
||||
&self,
|
||||
auth_code: String,
|
||||
original_redirect_uri: Option<String>,
|
||||
client_id: String,
|
||||
client_secret: &str,
|
||||
) -> ServiceResponse {
|
||||
self.call(
|
||||
TestRequest::post()
|
||||
.uri("/_internal/oauth/token")
|
||||
.append_header((AUTHORIZATION, client_secret))
|
||||
.set_form(TokenRequest {
|
||||
grant_type: "authorization_code".to_string(),
|
||||
code: auth_code,
|
||||
redirect_uri: original_redirect_uri,
|
||||
client_id: serde_json::from_str(&format!("\"{}\"", client_id)).unwrap(),
|
||||
})
|
||||
.to_request(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_authorize_uri(
|
||||
client_id: &str,
|
||||
scope: Option<&str>,
|
||||
redirect_uri: Option<&str>,
|
||||
state: Option<&str>,
|
||||
) -> String {
|
||||
format!(
|
||||
"/_internal/oauth/authorize?client_id={}{}{}{}",
|
||||
urlencoding::encode(client_id),
|
||||
optional_query_param("redirect_uri", redirect_uri),
|
||||
optional_query_param("scope", scope),
|
||||
optional_query_param("state", state),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn get_authorize_accept_flow_id(response: ServiceResponse) -> String {
|
||||
assert_status!(&response, StatusCode::OK);
|
||||
test::read_body_json::<OAuthClientAccessRequest, _>(response)
|
||||
.await
|
||||
.flow_id
|
||||
}
|
||||
|
||||
pub async fn get_auth_code_from_redirect_params(response: &ServiceResponse) -> String {
|
||||
assert_status!(response, StatusCode::OK);
|
||||
let query_params = get_redirect_location_query_params(response);
|
||||
query_params.get("code").unwrap().to_string()
|
||||
}
|
||||
|
||||
pub async fn get_access_token(response: ServiceResponse) -> String {
|
||||
assert_status!(&response, StatusCode::OK);
|
||||
test::read_body_json::<TokenResponse, _>(response)
|
||||
.await
|
||||
.access_token
|
||||
}
|
||||
|
||||
pub fn get_redirect_location_query_params(
|
||||
response: &ServiceResponse,
|
||||
) -> actix_web::web::Query<HashMap<String, String>> {
|
||||
let redirect_location = response
|
||||
.headers()
|
||||
.get(LOCATION)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
actix_web::web::Query::<HashMap<String, String>>::from_query(
|
||||
redirect_location.split_once('?').unwrap().1,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn optional_query_param(key: &str, value: Option<&str>) -> String {
|
||||
if let Some(val) = value {
|
||||
format!("&{key}={}", urlencoding::encode(val))
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
123
apps/labrinth/tests/common/api_v3/oauth_clients.rs
Normal file
123
apps/labrinth/tests/common/api_v3/oauth_clients.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{
|
||||
dev::ServiceResponse,
|
||||
test::{self, TestRequest},
|
||||
};
|
||||
use labrinth::{
|
||||
models::{
|
||||
oauth_clients::{OAuthClient, OAuthClientAuthorization},
|
||||
pats::Scopes,
|
||||
},
|
||||
routes::v3::oauth_clients::OAuthClientEdit,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::api_common::{Api, AppendsOptionalPat},
|
||||
};
|
||||
|
||||
use super::ApiV3;
|
||||
|
||||
impl ApiV3 {
|
||||
pub async fn add_oauth_client(
|
||||
&self,
|
||||
name: String,
|
||||
max_scopes: Scopes,
|
||||
redirect_uris: Vec<String>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let max_scopes = max_scopes.bits();
|
||||
let req = TestRequest::post()
|
||||
.uri("/_internal/oauth/app")
|
||||
.append_pat(pat)
|
||||
.set_json(json!({
|
||||
"name": name,
|
||||
"max_scopes": max_scopes,
|
||||
"redirect_uris": redirect_uris
|
||||
}))
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_user_oauth_clients(
|
||||
&self,
|
||||
user_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<OAuthClient> {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/user/{}/oauth_apps", user_id))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_oauth_client(&self, client_id: String, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/_internal/oauth/app/{}", client_id))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn edit_oauth_client(
|
||||
&self,
|
||||
client_id: &str,
|
||||
edit: OAuthClientEdit,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::patch()
|
||||
.uri(&format!(
|
||||
"/_internal/oauth/app/{}",
|
||||
urlencoding::encode(client_id)
|
||||
))
|
||||
.set_json(edit)
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn delete_oauth_client(&self, client_id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = TestRequest::delete()
|
||||
.uri(&format!("/_internal/oauth/app/{}", client_id))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn revoke_oauth_authorization(
|
||||
&self,
|
||||
client_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::delete()
|
||||
.uri(&format!(
|
||||
"/_internal/oauth/authorizations?client_id={}",
|
||||
urlencoding::encode(client_id)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_user_oauth_authorizations(
|
||||
&self,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<OAuthClientAuthorization> {
|
||||
let req = TestRequest::get()
|
||||
.uri("/_internal/oauth/authorizations")
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
}
|
||||
186
apps/labrinth/tests/common/api_v3/organization.rs
Normal file
186
apps/labrinth/tests/common/api_v3/organization.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{
|
||||
dev::ServiceResponse,
|
||||
test::{self, TestRequest},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use labrinth::models::{organizations::Organization, users::UserId, v3::projects::Project};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::api_common::{request_data::ImageData, Api, AppendsOptionalPat},
|
||||
};
|
||||
|
||||
use super::ApiV3;
|
||||
|
||||
impl ApiV3 {
|
||||
pub async fn create_organization(
|
||||
&self,
|
||||
organization_title: &str,
|
||||
organization_slug: &str,
|
||||
description: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/v3/organization")
|
||||
.append_pat(pat)
|
||||
.set_json(json!({
|
||||
"name": organization_title,
|
||||
"slug": organization_slug,
|
||||
"description": description,
|
||||
}))
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_organization(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/organization/{id_or_title}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_organization_deserialized(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Organization {
|
||||
let resp = self.get_organization(id_or_title, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_organizations(
|
||||
&self,
|
||||
ids_or_titles: &[&str],
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let ids_or_titles = serde_json::to_string(ids_or_titles).unwrap();
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v3/organizations?ids={}",
|
||||
urlencoding::encode(&ids_or_titles)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_organization_projects(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/organization/{id_or_title}/projects"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_organization_projects_deserialized(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<Project> {
|
||||
let resp = self.get_organization_projects(id_or_title, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn edit_organization(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v3/organization/{id_or_title}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn edit_organization_icon(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
icon: Option<ImageData>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
if let Some(icon) = icon {
|
||||
// If an icon is provided, upload it
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!(
|
||||
"/v3/organization/{id_or_title}/icon?ext={ext}",
|
||||
ext = icon.extension
|
||||
))
|
||||
.append_pat(pat)
|
||||
.set_payload(Bytes::from(icon.icon))
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
} else {
|
||||
// If no icon is provided, delete the icon
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/organization/{id_or_title}/icon"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn delete_organization(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/organization/{id_or_title}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn organization_add_project(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
project_id_or_slug: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&format!("/v3/organization/{id_or_title}/projects"))
|
||||
.append_pat(pat)
|
||||
.set_json(json!({
|
||||
"project_id": project_id_or_slug,
|
||||
}))
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn organization_remove_project(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
project_id_or_slug: &str,
|
||||
new_owner_user_id: UserId,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!(
|
||||
"/v3/organization/{id_or_title}/projects/{project_id_or_slug}"
|
||||
))
|
||||
.set_json(json!({
|
||||
"new_owner": new_owner_user_id,
|
||||
}))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
}
|
||||
602
apps/labrinth/tests/common/api_v3/project.rs
Normal file
602
apps/labrinth/tests/common/api_v3/project.rs
Normal file
@@ -0,0 +1,602 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
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::{organizations::Organization, projects::Project},
|
||||
search::SearchResults,
|
||||
util::actix::AppendsMultipart,
|
||||
};
|
||||
use rust_decimal::Decimal;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::{
|
||||
api_common::{
|
||||
models::{CommonItemType, CommonProject, CommonVersion},
|
||||
request_data::{ImageData, ProjectCreationRequestData},
|
||||
Api, ApiProject, AppendsOptionalPat,
|
||||
},
|
||||
database::MOD_USER_PAT,
|
||||
dummy_data::TestFile,
|
||||
},
|
||||
};
|
||||
|
||||
use super::{
|
||||
request_data::{self, get_public_project_creation_data},
|
||||
ApiV3,
|
||||
};
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl ApiProject for ApiV3 {
|
||||
async fn add_public_project(
|
||||
&self,
|
||||
slug: &str,
|
||||
version_jar: Option<TestFile>,
|
||||
modify_json: Option<json_patch::Patch>,
|
||||
pat: Option<&str>,
|
||||
) -> (CommonProject, Vec<CommonVersion>) {
|
||||
let creation_data = get_public_project_creation_data(slug, version_jar, modify_json);
|
||||
|
||||
// Add a project.
|
||||
let slug = creation_data.slug.clone();
|
||||
let resp = self.create_project(creation_data, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
// Approve as a moderator.
|
||||
let req = TestRequest::patch()
|
||||
.uri(&format!("/v3/project/{}", slug))
|
||||
.append_pat(MOD_USER_PAT)
|
||||
.set_json(json!(
|
||||
{
|
||||
"status": "approved"
|
||||
}
|
||||
))
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let project = self.get_project(&slug, pat).await;
|
||||
let project = test::read_body_json(project).await;
|
||||
|
||||
// Get project's versions
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/project/{}/version", slug))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
let versions: Vec<CommonVersion> = test::read_body_json(resp).await;
|
||||
|
||||
(project, versions)
|
||||
}
|
||||
|
||||
async fn get_public_project_creation_data_json(
|
||||
&self,
|
||||
slug: &str,
|
||||
version_jar: Option<&TestFile>,
|
||||
) -> serde_json::Value {
|
||||
request_data::get_public_project_creation_data_json(slug, version_jar)
|
||||
}
|
||||
|
||||
async fn create_project(
|
||||
&self,
|
||||
creation_data: ProjectCreationRequestData,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::post()
|
||||
.uri("/v3/project")
|
||||
.append_pat(pat)
|
||||
.set_multipart(creation_data.segment_data)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn remove_project(&self, project_slug_or_id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/project/{project_slug_or_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_project(&self, id_or_slug: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/project/{id_or_slug}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_project_deserialized_common(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
pat: Option<&str>,
|
||||
) -> CommonProject {
|
||||
let resp = self.get_project(id_or_slug, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let project: Project = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(project).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_projects(&self, ids_or_slugs: &[&str], pat: Option<&str>) -> ServiceResponse {
|
||||
let ids_or_slugs = serde_json::to_string(ids_or_slugs).unwrap();
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v3/projects?ids={encoded}",
|
||||
encoded = urlencoding::encode(&ids_or_slugs)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_project_dependencies(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/project/{id_or_slug}/dependencies"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_user_projects(
|
||||
&self,
|
||||
user_id_or_username: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/user/{}/projects", user_id_or_username))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_user_projects_deserialized_common(
|
||||
&self,
|
||||
user_id_or_username: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonProject> {
|
||||
let resp = self.get_user_projects(user_id_or_username, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let projects: Vec<Project> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(projects).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn edit_project(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v3/project/{id_or_slug}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn edit_project_bulk(
|
||||
&self,
|
||||
ids_or_slugs: &[&str],
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let projects_str = ids_or_slugs
|
||||
.iter()
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!(
|
||||
"/v3/projects?ids={encoded}",
|
||||
encoded = urlencoding::encode(&format!("[{projects_str}]"))
|
||||
))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn edit_project_icon(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
icon: Option<ImageData>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
if let Some(icon) = icon {
|
||||
// If an icon is provided, upload it
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!(
|
||||
"/v3/project/{id_or_slug}/icon?ext={ext}",
|
||||
ext = icon.extension
|
||||
))
|
||||
.append_pat(pat)
|
||||
.set_payload(Bytes::from(icon.icon))
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
} else {
|
||||
// If no icon is provided, delete the icon
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/project/{id_or_slug}/icon"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_report(
|
||||
&self,
|
||||
report_type: &str,
|
||||
id: &str,
|
||||
item_type: CommonItemType,
|
||||
body: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/v3/report")
|
||||
.append_pat(pat)
|
||||
.set_json(json!(
|
||||
{
|
||||
"report_type": report_type,
|
||||
"item_id": id,
|
||||
"item_type": item_type.as_str(),
|
||||
"body": body,
|
||||
}
|
||||
))
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_report(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/report/{id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_reports(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse {
|
||||
let ids_str = serde_json::to_string(ids).unwrap();
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v3/reports?ids={encoded}",
|
||||
encoded = urlencoding::encode(&ids_str)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_user_reports(&self, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/v3/report")
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn edit_report(
|
||||
&self,
|
||||
id: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v3/report/{id}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn delete_report(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/report/{id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn add_gallery_item(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
image: ImageData,
|
||||
featured: bool,
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
ordering: Option<i32>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let mut url = format!(
|
||||
"/v3/project/{id_or_slug}/gallery?ext={ext}&featured={featured}",
|
||||
ext = image.extension,
|
||||
featured = featured
|
||||
);
|
||||
if let Some(title) = title {
|
||||
url.push_str(&format!("&title={}", title));
|
||||
}
|
||||
if let Some(description) = description {
|
||||
url.push_str(&format!("&description={}", description));
|
||||
}
|
||||
if let Some(ordering) = ordering {
|
||||
url.push_str(&format!("&ordering={}", ordering));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&url)
|
||||
.append_pat(pat)
|
||||
.set_payload(Bytes::from(image.icon))
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn edit_gallery_item(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
image_url: &str,
|
||||
patch: HashMap<String, String>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let mut url = format!(
|
||||
"/v3/project/{id_or_slug}/gallery?url={image_url}",
|
||||
image_url = urlencoding::encode(image_url)
|
||||
);
|
||||
|
||||
for (key, value) in patch {
|
||||
url.push_str(&format!(
|
||||
"&{key}={value}",
|
||||
key = key,
|
||||
value = urlencoding::encode(&value)
|
||||
));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&url)
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn remove_gallery_item(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
url: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!(
|
||||
"/v3/project/{id_or_slug}/gallery?url={url}",
|
||||
url = url
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_thread(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/thread/{id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_threads(&self, ids: &[&str], pat: Option<&str>) -> ServiceResponse {
|
||||
let ids_str = serde_json::to_string(ids).unwrap();
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v3/threads?ids={encoded}",
|
||||
encoded = urlencoding::encode(&ids_str)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn write_to_thread(
|
||||
&self,
|
||||
id: &str,
|
||||
r#type: &str,
|
||||
content: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&format!("/v3/thread/{id}"))
|
||||
.append_pat(pat)
|
||||
.set_json(json!({
|
||||
"body": {
|
||||
"type": r#type,
|
||||
"body": content
|
||||
}
|
||||
}))
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_moderation_inbox(&self, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/v3/thread/inbox")
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn read_thread(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&format!("/v3/thread/{id}/read"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn delete_thread_message(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/message/{id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiV3 {
|
||||
pub async fn get_project_deserialized(&self, id_or_slug: &str, pat: Option<&str>) -> Project {
|
||||
let resp = self.get_project(id_or_slug, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_project_organization(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/project/{id_or_slug}/organization"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_project_organization_deserialized(
|
||||
&self,
|
||||
id_or_slug: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Organization {
|
||||
let resp = self.get_project_organization(id_or_slug, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn search_deserialized(
|
||||
&self,
|
||||
query: Option<&str>,
|
||||
facets: Option<serde_json::Value>,
|
||||
pat: Option<&str>,
|
||||
) -> SearchResults {
|
||||
let query_field = if let Some(query) = query {
|
||||
format!("&query={}", urlencoding::encode(query))
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let facets_field = if let Some(facets) = facets {
|
||||
format!("&facets={}", urlencoding::encode(&facets.to_string()))
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/search?{}{}", query_field, facets_field))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
let resp = self.call(req).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_analytics_revenue(
|
||||
&self,
|
||||
id_or_slugs: Vec<&str>,
|
||||
ids_are_version_ids: bool,
|
||||
start_date: Option<DateTime<Utc>>,
|
||||
end_date: Option<DateTime<Utc>>,
|
||||
resolution_minutes: Option<u32>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let pv_string = if ids_are_version_ids {
|
||||
let version_string: String = serde_json::to_string(&id_or_slugs).unwrap();
|
||||
let version_string = urlencoding::encode(&version_string);
|
||||
format!("version_ids={}", version_string)
|
||||
} else {
|
||||
let projects_string: String = serde_json::to_string(&id_or_slugs).unwrap();
|
||||
let projects_string = urlencoding::encode(&projects_string);
|
||||
format!("project_ids={}", 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!("/v3/analytics/revenue?{pv_string}{extra_args}",))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_analytics_revenue_deserialized(
|
||||
&self,
|
||||
id_or_slugs: Vec<&str>,
|
||||
ids_are_version_ids: bool,
|
||||
start_date: Option<DateTime<Utc>>,
|
||||
end_date: Option<DateTime<Utc>>,
|
||||
resolution_minutes: Option<u32>,
|
||||
pat: Option<&str>,
|
||||
) -> HashMap<String, HashMap<i64, Decimal>> {
|
||||
let resp = self
|
||||
.get_analytics_revenue(
|
||||
id_or_slugs,
|
||||
ids_are_version_ids,
|
||||
start_date,
|
||||
end_date,
|
||||
resolution_minutes,
|
||||
pat,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
}
|
||||
135
apps/labrinth/tests/common/api_v3/request_data.rs
Normal file
135
apps/labrinth/tests/common/api_v3/request_data.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
#![allow(dead_code)]
|
||||
use serde_json::json;
|
||||
|
||||
use crate::common::{
|
||||
api_common::request_data::{ProjectCreationRequestData, VersionCreationRequestData},
|
||||
dummy_data::TestFile,
|
||||
};
|
||||
use labrinth::{
|
||||
models::projects::ProjectId,
|
||||
util::actix::{MultipartSegment, MultipartSegmentData},
|
||||
};
|
||||
|
||||
pub fn get_public_project_creation_data(
|
||||
slug: &str,
|
||||
version_jar: Option<TestFile>,
|
||||
modify_json: Option<json_patch::Patch>,
|
||||
) -> ProjectCreationRequestData {
|
||||
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(),
|
||||
jar: version_jar,
|
||||
segment_data: multipart_data,
|
||||
}
|
||||
}
|
||||
|
||||
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<json_patch::Patch>,
|
||||
) -> VersionCreationRequestData {
|
||||
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(),
|
||||
jar: Some(version_jar),
|
||||
segment_data: multipart_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";
|
||||
let mut j = json!({
|
||||
"file_parts": [version_jar.filename()],
|
||||
"version_number": version_number,
|
||||
"version_title": "start",
|
||||
"dependencies": [],
|
||||
"release_channel": "release",
|
||||
"loaders": [if is_modpack { "mrpack" } else { "fabric" }],
|
||||
"featured": true,
|
||||
|
||||
// Loader fields
|
||||
"game_versions": ["1.20.1"],
|
||||
"singleplayer": true,
|
||||
"client_and_server": true,
|
||||
"client_only": true,
|
||||
"server_only": false,
|
||||
});
|
||||
if is_modpack {
|
||||
j["mrpack_loaders"] = json!(["fabric"]);
|
||||
}
|
||||
if let Some(ordering) = ordering {
|
||||
j["ordering"] = json!(ordering);
|
||||
}
|
||||
j
|
||||
}
|
||||
|
||||
pub fn get_public_project_creation_data_json(
|
||||
slug: &str,
|
||||
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", None, jar)])
|
||||
} else {
|
||||
json!([])
|
||||
};
|
||||
|
||||
let is_draft = version_jar.is_none();
|
||||
json!(
|
||||
{
|
||||
"name": format!("Test Project {slug}"),
|
||||
"slug": slug,
|
||||
"summary": "A dummy project for testing with.",
|
||||
"description": "This project is approved, and versions are listed.",
|
||||
"initial_versions": initial_versions,
|
||||
"is_draft": is_draft,
|
||||
"categories": [],
|
||||
"license_id": "MIT",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_public_creation_data_multipart(
|
||||
json_data: &serde_json::Value,
|
||||
version_jar: Option<&TestFile>,
|
||||
) -> Vec<MultipartSegment> {
|
||||
// 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()),
|
||||
};
|
||||
|
||||
if let Some(jar) = version_jar {
|
||||
// Basic file
|
||||
let file_segment = MultipartSegment {
|
||||
name: jar.filename(),
|
||||
filename: Some(jar.filename()),
|
||||
content_type: Some("application/java-archive".to_string()),
|
||||
data: MultipartSegmentData::Binary(jar.bytes()),
|
||||
};
|
||||
|
||||
vec![json_segment, file_segment]
|
||||
} else {
|
||||
vec![json_segment]
|
||||
}
|
||||
}
|
||||
101
apps/labrinth/tests/common/api_v3/tags.rs
Normal file
101
apps/labrinth/tests/common/api_v3/tags.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{
|
||||
dev::ServiceResponse,
|
||||
test::{self, TestRequest},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use labrinth::routes::v3::tags::{GameData, LoaderData};
|
||||
use labrinth::{
|
||||
database::models::loader_fields::LoaderFieldEnumValue, routes::v3::tags::CategoryData,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::{
|
||||
api_common::{
|
||||
models::{CommonCategoryData, CommonLoaderData},
|
||||
Api, ApiTags, AppendsOptionalPat,
|
||||
},
|
||||
database::ADMIN_USER_PAT,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ApiV3;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl ApiTags for ApiV3 {
|
||||
async fn get_loaders(&self) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri("/v3/tag/loader")
|
||||
.append_pat(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_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<LoaderData> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_categories(&self) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri("/v3/tag/category")
|
||||
.append_pat(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_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<CategoryData> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl ApiV3 {
|
||||
pub async fn get_loaders_deserialized(&self) -> Vec<LoaderData> {
|
||||
let resp = self.get_loaders().await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_loader_field_variants(&self, loader_field: &str) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/loader_field?loader_field={}", loader_field))
|
||||
.append_pat(ADMIN_USER_PAT)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_loader_field_variants_deserialized(
|
||||
&self,
|
||||
loader_field: &str,
|
||||
) -> Vec<LoaderFieldEnumValue> {
|
||||
let resp = self.get_loader_field_variants(loader_field).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
async fn get_games(&self) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri("/v3/games")
|
||||
.append_pat(ADMIN_USER_PAT)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
pub async fn get_games_deserialized(&self) -> Vec<GameData> {
|
||||
let resp = self.get_games().await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
}
|
||||
313
apps/labrinth/tests/common/api_v3/team.rs
Normal file
313
apps/labrinth/tests/common/api_v3/team.rs
Normal file
@@ -0,0 +1,313 @@
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{dev::ServiceResponse, test};
|
||||
use async_trait::async_trait;
|
||||
use labrinth::models::{
|
||||
notifications::Notification,
|
||||
teams::{OrganizationPermissions, ProjectPermissions, TeamMember},
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::api_common::{
|
||||
models::{CommonNotification, CommonTeamMember},
|
||||
Api, ApiTeams, AppendsOptionalPat,
|
||||
},
|
||||
};
|
||||
|
||||
use super::ApiV3;
|
||||
|
||||
impl ApiV3 {
|
||||
pub async fn get_organization_members_deserialized(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<TeamMember> {
|
||||
let resp = self.get_organization_members(id_or_title, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_team_members_deserialized(
|
||||
&self,
|
||||
team_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<TeamMember> {
|
||||
let resp = self.get_team_members(team_id, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_project_members_deserialized(
|
||||
&self,
|
||||
project_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<TeamMember> {
|
||||
let resp = self.get_project_members(project_id, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl ApiTeams for ApiV3 {
|
||||
async fn get_team_members(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/team/{id_or_title}/members"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_team_members_deserialized_common(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonTeamMember> {
|
||||
let resp = self.get_team_members(id_or_title, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<TeamMember> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_teams_members(
|
||||
&self,
|
||||
ids_or_titles: &[&str],
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let ids_or_titles = serde_json::to_string(ids_or_titles).unwrap();
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v3/teams?ids={}",
|
||||
urlencoding::encode(&ids_or_titles)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_project_members(&self, id_or_title: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/project/{id_or_title}/members"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_project_members_deserialized_common(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonTeamMember> {
|
||||
let resp = self.get_project_members(id_or_title, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<TeamMember> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_organization_members(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/organization/{id_or_title}/members"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_organization_members_deserialized_common(
|
||||
&self,
|
||||
id_or_title: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonTeamMember> {
|
||||
let resp = self.get_organization_members(id_or_title, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<TeamMember> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn join_team(&self, team_id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&format!("/v3/team/{team_id}/join"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn remove_from_team(
|
||||
&self,
|
||||
team_id: &str,
|
||||
user_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/team/{team_id}/members/{user_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn edit_team_member(
|
||||
&self,
|
||||
team_id: &str,
|
||||
user_id: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v3/team/{team_id}/members/{user_id}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn transfer_team_ownership(
|
||||
&self,
|
||||
team_id: &str,
|
||||
user_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v3/team/{team_id}/owner"))
|
||||
.append_pat(pat)
|
||||
.set_json(json!({
|
||||
"user_id": user_id,
|
||||
}))
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_user_notifications(&self, user_id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/user/{user_id}/notifications"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_user_notifications_deserialized_common(
|
||||
&self,
|
||||
user_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonNotification> {
|
||||
let resp = self.get_user_notifications(user_id, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<Notification> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_notification(&self, notification_id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/notification/{notification_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_notifications(
|
||||
&self,
|
||||
notification_ids: &[&str],
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let notification_ids = serde_json::to_string(notification_ids).unwrap();
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v3/notifications?ids={}",
|
||||
urlencoding::encode(¬ification_ids)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn mark_notification_read(
|
||||
&self,
|
||||
notification_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v3/notification/{notification_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn mark_notifications_read(
|
||||
&self,
|
||||
notification_ids: &[&str],
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let notification_ids = serde_json::to_string(notification_ids).unwrap();
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!(
|
||||
"/v3/notifications?ids={}",
|
||||
urlencoding::encode(¬ification_ids)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn add_user_to_team(
|
||||
&self,
|
||||
team_id: &str,
|
||||
user_id: &str,
|
||||
project_permissions: Option<ProjectPermissions>,
|
||||
organization_permissions: Option<OrganizationPermissions>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&format!("/v3/team/{team_id}/members"))
|
||||
.append_pat(pat)
|
||||
.set_json(json!( {
|
||||
"user_id": user_id,
|
||||
"permissions" : project_permissions.map(|p| p.bits()).unwrap_or_default(),
|
||||
"organization_permissions" : organization_permissions.map(|p| p.bits()),
|
||||
}))
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn delete_notification(
|
||||
&self,
|
||||
notification_id: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/notification/{notification_id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn delete_notifications(
|
||||
&self,
|
||||
notification_ids: &[&str],
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let notification_ids = serde_json::to_string(notification_ids).unwrap();
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!(
|
||||
"/v3/notifications?ids={}",
|
||||
urlencoding::encode(¬ification_ids)
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
}
|
||||
48
apps/labrinth/tests/common/api_v3/user.rs
Normal file
48
apps/labrinth/tests/common/api_v3/user.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use actix_web::{dev::ServiceResponse, test};
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::common::api_common::{Api, ApiUser, AppendsOptionalPat};
|
||||
|
||||
use super::ApiV3;
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl ApiUser for ApiV3 {
|
||||
async fn get_user(&self, user_id_or_username: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/user/{}", user_id_or_username))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_current_user(&self, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri("/v3/user")
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn edit_user(
|
||||
&self,
|
||||
user_id_or_username: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v3/user/{}", user_id_or_username))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn delete_user(&self, user_id_or_username: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/user/{}", user_id_or_username))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
}
|
||||
547
apps/labrinth/tests/common/api_v3/version.rs
Normal file
547
apps/labrinth/tests/common/api_v3/version.rs
Normal file
@@ -0,0 +1,547 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::{
|
||||
request_data::{self, get_public_version_creation_data},
|
||||
ApiV3,
|
||||
};
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::{
|
||||
api_common::{models::CommonVersion, Api, ApiVersion, AppendsOptionalPat},
|
||||
dummy_data::TestFile,
|
||||
},
|
||||
};
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::{
|
||||
dev::ServiceResponse,
|
||||
test::{self, TestRequest},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use labrinth::{
|
||||
models::{
|
||||
projects::{ProjectId, VersionType},
|
||||
v3::projects::Version,
|
||||
},
|
||||
routes::v3::version_file::FileUpdateData,
|
||||
util::actix::AppendsMultipart,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
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_deserialized(
|
||||
&self,
|
||||
project_id: ProjectId,
|
||||
version_number: &str,
|
||||
version_jar: TestFile,
|
||||
ordering: Option<i32>,
|
||||
modify_json: Option<json_patch::Patch>,
|
||||
pat: Option<&str>,
|
||||
) -> Version {
|
||||
let resp = self
|
||||
.add_public_version(
|
||||
project_id,
|
||||
version_number,
|
||||
version_jar,
|
||||
ordering,
|
||||
modify_json,
|
||||
pat,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
let value: serde_json::Value = test::read_body_json(resp).await;
|
||||
let version_id = value["id"].as_str().unwrap();
|
||||
let version = self.get_version(version_id, pat).await;
|
||||
assert_status!(&version, StatusCode::OK);
|
||||
test::read_body_json(version).await
|
||||
}
|
||||
|
||||
pub async fn get_version_deserialized(&self, id: &str, pat: Option<&str>) -> Version {
|
||||
let resp = self.get_version(id, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn get_versions_deserialized(
|
||||
&self,
|
||||
version_ids: Vec<String>,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<Version> {
|
||||
let resp = self.get_versions(version_ids, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
test::read_body_json(resp).await
|
||||
}
|
||||
|
||||
pub async fn update_individual_files(
|
||||
&self,
|
||||
algorithm: &str,
|
||||
hashes: Vec<FileUpdateData>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/v3/version_files/update_individual")
|
||||
.append_pat(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: Option<&str>,
|
||||
) -> HashMap<String, Version> {
|
||||
let resp = self.update_individual_files(algorithm, hashes, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
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: Option<&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_pat(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: Option<&str>,
|
||||
) -> CommonVersion {
|
||||
let resp = self
|
||||
.add_public_version(
|
||||
project_id,
|
||||
version_number,
|
||||
version_jar,
|
||||
ordering,
|
||||
modify_json,
|
||||
pat,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Version = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_version(&self, id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/v3/version/{id}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_version_deserialized_common(&self, id: &str, pat: Option<&str>) -> CommonVersion {
|
||||
let resp = self.get_version(id, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Version = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn edit_version(
|
||||
&self,
|
||||
version_id: &str,
|
||||
patch: serde_json::Value,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/v3/version/{version_id}"))
|
||||
.append_pat(pat)
|
||||
.set_json(patch)
|
||||
.to_request();
|
||||
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn download_version_redirect(
|
||||
&self,
|
||||
hash: &str,
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/version_file/{hash}/download",))
|
||||
.set_json(json!({
|
||||
"algorithm": algorithm,
|
||||
}))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_version_from_hash(
|
||||
&self,
|
||||
hash: &str,
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!("/v3/version_file/{hash}?algorithm={algorithm}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
async fn get_version_from_hash_deserialized_common(
|
||||
&self,
|
||||
hash: &str,
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> CommonVersion {
|
||||
let resp = self.get_version_from_hash(hash, algorithm, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Version = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn get_versions_from_hashes(
|
||||
&self,
|
||||
hashes: &[&str],
|
||||
algorithm: &str,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let req = TestRequest::post()
|
||||
.uri("/v3/version_files")
|
||||
.append_pat(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: Option<&str>,
|
||||
) -> HashMap<String, CommonVersion> {
|
||||
let resp = self.get_versions_from_hashes(hashes, algorithm, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: HashMap<String, Version> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
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: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let mut json = json!({});
|
||||
if let Some(loaders) = loaders {
|
||||
json["loaders"] = serde_json::to_value(loaders).unwrap();
|
||||
}
|
||||
if let Some(game_versions) = game_versions {
|
||||
json["loader_fields"] = json!({
|
||||
"game_versions": game_versions,
|
||||
});
|
||||
}
|
||||
if let Some(version_types) = version_types {
|
||||
json["version_types"] = serde_json::to_value(version_types).unwrap();
|
||||
}
|
||||
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&format!(
|
||||
"/v3/version_file/{hash}/update?algorithm={algorithm}"
|
||||
))
|
||||
.append_pat(pat)
|
||||
.set_json(json)
|
||||
.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: Option<&str>,
|
||||
) -> CommonVersion {
|
||||
let resp = self
|
||||
.get_update_from_hash(hash, algorithm, loaders, game_versions, version_types, pat)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Version = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
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: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let mut json = json!({
|
||||
"algorithm": algorithm,
|
||||
"hashes": hashes,
|
||||
});
|
||||
if let Some(loaders) = loaders {
|
||||
json["loaders"] = serde_json::to_value(loaders).unwrap();
|
||||
}
|
||||
if let Some(game_versions) = game_versions {
|
||||
json["game_versions"] = serde_json::to_value(game_versions).unwrap();
|
||||
}
|
||||
if let Some(version_types) = version_types {
|
||||
json["version_types"] = serde_json::to_value(version_types).unwrap();
|
||||
}
|
||||
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/v3/version_files/update")
|
||||
.append_pat(pat)
|
||||
.set_json(json)
|
||||
.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: Option<&str>,
|
||||
) -> HashMap<String, CommonVersion> {
|
||||
let resp = self
|
||||
.update_files(
|
||||
algorithm,
|
||||
hashes,
|
||||
loaders,
|
||||
game_versions,
|
||||
version_types,
|
||||
pat,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: HashMap<String, Version> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
// TODO: Not all fields are tested currently in the v3 tests, only the v2-v3 relevant ones are
|
||||
#[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: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let mut query_string = String::new();
|
||||
if let Some(game_versions) = game_versions {
|
||||
query_string.push_str(&format!(
|
||||
"&game_versions={}",
|
||||
urlencoding::encode(&serde_json::to_string(&game_versions).unwrap())
|
||||
));
|
||||
}
|
||||
if let Some(loaders) = loaders {
|
||||
query_string.push_str(&format!(
|
||||
"&loaders={}",
|
||||
urlencoding::encode(&serde_json::to_string(&loaders).unwrap())
|
||||
));
|
||||
}
|
||||
if let Some(featured) = featured {
|
||||
query_string.push_str(&format!("&featured={}", featured));
|
||||
}
|
||||
if let Some(version_type) = version_type {
|
||||
query_string.push_str(&format!("&version_type={}", version_type));
|
||||
}
|
||||
if let Some(limit) = limit {
|
||||
let limit = limit.to_string();
|
||||
query_string.push_str(&format!("&limit={}", limit));
|
||||
}
|
||||
if let Some(offset) = offset {
|
||||
let offset = offset.to_string();
|
||||
query_string.push_str(&format!("&offset={}", offset));
|
||||
}
|
||||
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&format!(
|
||||
"/v3/project/{project_id_slug}/version?{}",
|
||||
query_string.trim_start_matches('&')
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(req).await
|
||||
}
|
||||
|
||||
#[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: Option<&str>,
|
||||
) -> Vec<CommonVersion> {
|
||||
let resp = self
|
||||
.get_project_versions(
|
||||
slug,
|
||||
game_versions,
|
||||
loaders,
|
||||
featured,
|
||||
version_type,
|
||||
limit,
|
||||
offset,
|
||||
pat,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<Version> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn edit_version_ordering(
|
||||
&self,
|
||||
version_id: &str,
|
||||
ordering: Option<i32>,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let request = test::TestRequest::patch()
|
||||
.uri(&format!("/v3/version/{version_id}"))
|
||||
.set_json(json!(
|
||||
{
|
||||
"ordering": ordering
|
||||
}
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
}
|
||||
|
||||
async fn get_versions(&self, version_ids: Vec<String>, pat: Option<&str>) -> ServiceResponse {
|
||||
let ids = url_encode_json_serialized_vec(&version_ids);
|
||||
let request = test::TestRequest::get()
|
||||
.uri(&format!("/v3/versions?ids={}", ids))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
}
|
||||
|
||||
async fn get_versions_deserialized_common(
|
||||
&self,
|
||||
version_ids: Vec<String>,
|
||||
pat: Option<&str>,
|
||||
) -> Vec<CommonVersion> {
|
||||
let resp = self.get_versions(version_ids, pat).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
// First, deserialize to the non-common format (to test the response is valid for this api version)
|
||||
let v: Vec<Version> = test::read_body_json(resp).await;
|
||||
// Then, deserialize to the common format
|
||||
let value = serde_json::to_value(v).unwrap();
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
async fn upload_file_to_version(
|
||||
&self,
|
||||
version_id: &str,
|
||||
file: &TestFile,
|
||||
pat: Option<&str>,
|
||||
) -> ServiceResponse {
|
||||
let m = request_data::get_public_creation_data_multipart(
|
||||
&json!({
|
||||
"file_parts": [file.filename()]
|
||||
}),
|
||||
Some(file),
|
||||
);
|
||||
let request = test::TestRequest::post()
|
||||
.uri(&format!(
|
||||
"/v3/version/{version_id}/file",
|
||||
version_id = version_id
|
||||
))
|
||||
.append_pat(pat)
|
||||
.set_multipart(m)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
}
|
||||
|
||||
async fn remove_version(&self, version_id: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let request = test::TestRequest::delete()
|
||||
.uri(&format!(
|
||||
"/v3/version/{version_id}",
|
||||
version_id = version_id
|
||||
))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
}
|
||||
|
||||
async fn remove_version_file(&self, hash: &str, pat: Option<&str>) -> ServiceResponse {
|
||||
let request = test::TestRequest::delete()
|
||||
.uri(&format!("/v3/version_file/{hash}"))
|
||||
.append_pat(pat)
|
||||
.to_request();
|
||||
self.call(request).await
|
||||
}
|
||||
}
|
||||
47
apps/labrinth/tests/common/asserts.rs
Normal file
47
apps/labrinth/tests/common/asserts.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::common::get_json_val_str;
|
||||
use itertools::Itertools;
|
||||
use labrinth::models::v3::projects::Version;
|
||||
|
||||
use super::api_common::models::CommonVersion;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_status {
|
||||
($response:expr, $status:expr) => {
|
||||
assert_eq!(
|
||||
$response.status(),
|
||||
$status,
|
||||
"{:#?}",
|
||||
$response.response().body()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! assert_any_status_except {
|
||||
($response:expr, $status:expr) => {
|
||||
assert_ne!(
|
||||
$response.status(),
|
||||
$status,
|
||||
"{:#?}",
|
||||
$response.response().body()
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
pub fn assert_version_ids(versions: &[Version], 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_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);
|
||||
}
|
||||
258
apps/labrinth/tests/common/database.rs
Normal file
258
apps/labrinth/tests/common/database.rs
Normal file
@@ -0,0 +1,258 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use labrinth::{database::redis::RedisPool, search};
|
||||
use sqlx::{postgres::PgPoolOptions, PgPool};
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
|
||||
use crate::common::{dummy_data, environment::TestEnvironment};
|
||||
|
||||
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.
|
||||
// The rest can be accessed in the TestEnvironment 'dummy' field.
|
||||
|
||||
// The user IDs are as follows:
|
||||
pub const ADMIN_USER_ID: &str = "1";
|
||||
pub const MOD_USER_ID: &str = "2";
|
||||
pub const USER_USER_ID: &str = "3"; // This is the 'main' user ID, and is used for most tests.
|
||||
pub const FRIEND_USER_ID: &str = "4"; // This is exactly the same as USER_USER_ID, but could be used for testing friend-only endpoints (ie: teams, etc)
|
||||
pub const ENEMY_USER_ID: &str = "5"; // This is exactly the same as USER_USER_ID, but could be used for testing friend-only endpoints (ie: teams, etc)
|
||||
|
||||
pub const ADMIN_USER_ID_PARSED: i64 = 1;
|
||||
pub const MOD_USER_ID_PARSED: i64 = 2;
|
||||
pub const USER_USER_ID_PARSED: i64 = 3;
|
||||
pub const FRIEND_USER_ID_PARSED: i64 = 4;
|
||||
pub const ENEMY_USER_ID_PARSED: i64 = 5;
|
||||
|
||||
// These are full-scoped PATs- as if the user was logged in (including illegal scopes).
|
||||
pub const ADMIN_USER_PAT: Option<&str> = Some("mrp_patadmin");
|
||||
pub const MOD_USER_PAT: Option<&str> = Some("mrp_patmoderator");
|
||||
pub const USER_USER_PAT: Option<&str> = Some("mrp_patuser");
|
||||
pub const FRIEND_USER_PAT: Option<&str> = Some("mrp_patfriend");
|
||||
pub const ENEMY_USER_PAT: Option<&str> = Some("mrp_patenemy");
|
||||
|
||||
const TEMPLATE_DATABASE_NAME: &str = "labrinth_tests_template";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TemporaryDatabase {
|
||||
pub pool: PgPool,
|
||||
pub redis_pool: RedisPool,
|
||||
pub search_config: labrinth::search::SearchConfig,
|
||||
pub database_name: String,
|
||||
}
|
||||
|
||||
impl TemporaryDatabase {
|
||||
// Creates a temporary database like sqlx::test does (panics)
|
||||
// 1. Logs into the main database
|
||||
// 2. Creates a new randomly generated database
|
||||
// 3. Runs migrations on the new database
|
||||
// 4. (Optionally, by using create_with_dummy) adds dummy data to the database
|
||||
// If a db is created with create_with_dummy, it must be cleaned up with cleanup.
|
||||
// This means that dbs will only 'remain' if a test fails (for examination of the db), and will be cleaned up otherwise.
|
||||
pub async fn create(max_connections: Option<u32>) -> Self {
|
||||
let temp_database_name = generate_random_name("labrinth_tests_db_");
|
||||
println!("Creating temporary database: {}", &temp_database_name);
|
||||
|
||||
let database_url = dotenvy::var("DATABASE_URL").expect("No database URL");
|
||||
|
||||
// Create the temporary (and template datbase, if needed)
|
||||
Self::create_temporary(&database_url, &temp_database_name).await;
|
||||
|
||||
// Pool to the temporary database
|
||||
let mut temporary_url = Url::parse(&database_url).expect("Invalid database URL");
|
||||
|
||||
temporary_url.set_path(&format!("/{}", &temp_database_name));
|
||||
let temp_db_url = temporary_url.to_string();
|
||||
|
||||
let pool = PgPoolOptions::new()
|
||||
.min_connections(0)
|
||||
.max_connections(max_connections.unwrap_or(4))
|
||||
.max_lifetime(Some(Duration::from_secs(60)))
|
||||
.connect(&temp_db_url)
|
||||
.await
|
||||
.expect("Connection to temporary database failed");
|
||||
|
||||
println!("Running migrations on temporary database");
|
||||
|
||||
// Performs migrations
|
||||
let migrations = sqlx::migrate!("./migrations");
|
||||
migrations.run(&pool).await.expect("Migrations failed");
|
||||
|
||||
println!("Migrations complete");
|
||||
|
||||
// Gets new Redis pool
|
||||
let redis_pool = RedisPool::new(Some(temp_database_name.clone()));
|
||||
|
||||
// Create new meilisearch config
|
||||
let search_config = search::SearchConfig::new(Some(temp_database_name.clone()));
|
||||
Self {
|
||||
pool,
|
||||
database_name: temp_database_name,
|
||||
redis_pool,
|
||||
search_config,
|
||||
}
|
||||
}
|
||||
|
||||
// Creates a template and temporary databse (panics)
|
||||
// 1. Waits to obtain a pg lock on the main database
|
||||
// 2. Creates a new template database called 'TEMPLATE_DATABASE_NAME', if needed
|
||||
// 3. Switches to the template database
|
||||
// 4. Runs migrations on the new database (for most tests, this should not take time)
|
||||
// 5. Creates dummy data on the new db
|
||||
// 6. Creates a temporary database at 'temp_database_name' from the template
|
||||
// 7. Drops lock and all created connections in the function
|
||||
async fn create_temporary(database_url: &str, temp_database_name: &str) {
|
||||
let main_pool = PgPool::connect(database_url)
|
||||
.await
|
||||
.expect("Connection to database failed");
|
||||
|
||||
loop {
|
||||
// Try to acquire an advisory lock
|
||||
let lock_acquired: bool = sqlx::query_scalar("SELECT pg_try_advisory_lock(1)")
|
||||
.fetch_one(&main_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if lock_acquired {
|
||||
// Create the db template if it doesn't exist
|
||||
// Check if template_db already exists
|
||||
let db_exists: Option<i32> = sqlx::query_scalar(&format!(
|
||||
"SELECT 1 FROM pg_database WHERE datname = '{TEMPLATE_DATABASE_NAME}'"
|
||||
))
|
||||
.fetch_optional(&main_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
if db_exists.is_none() {
|
||||
create_template_database(&main_pool).await;
|
||||
}
|
||||
|
||||
// Switch to template
|
||||
let url = dotenvy::var("DATABASE_URL").expect("No database URL");
|
||||
let mut template_url = Url::parse(&url).expect("Invalid database URL");
|
||||
template_url.set_path(&format!("/{}", TEMPLATE_DATABASE_NAME));
|
||||
|
||||
let pool = PgPool::connect(template_url.as_str())
|
||||
.await
|
||||
.expect("Connection to database failed");
|
||||
|
||||
// Check if dummy data exists- a fake 'dummy_data' table is created if it does
|
||||
let mut dummy_data_exists: bool =
|
||||
sqlx::query_scalar("SELECT to_regclass('dummy_data') IS NOT NULL")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
if dummy_data_exists {
|
||||
// Check if the dummy data needs to be updated
|
||||
let dummy_data_update =
|
||||
sqlx::query_scalar::<_, i64>("SELECT update_id FROM dummy_data")
|
||||
.fetch_optional(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
let needs_update = !dummy_data_update.is_some_and(|d| d == DUMMY_DATA_UPDATE);
|
||||
if needs_update {
|
||||
println!("Dummy data updated, so template DB tables will be dropped and re-created");
|
||||
// Drop all tables in the database so they can be re-created and later filled with updated dummy data
|
||||
sqlx::query("DROP SCHEMA public CASCADE;")
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
sqlx::query("CREATE SCHEMA public;")
|
||||
.execute(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
dummy_data_exists = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Run migrations on the template
|
||||
let migrations = sqlx::migrate!("./migrations");
|
||||
migrations.run(&pool).await.expect("Migrations failed");
|
||||
|
||||
if !dummy_data_exists {
|
||||
// Add dummy data
|
||||
let name = generate_random_name("test_template_");
|
||||
let db = TemporaryDatabase {
|
||||
pool: pool.clone(),
|
||||
database_name: TEMPLATE_DATABASE_NAME.to_string(),
|
||||
redis_pool: RedisPool::new(Some(name.clone())),
|
||||
search_config: search::SearchConfig::new(Some(name)),
|
||||
};
|
||||
let setup_api = TestEnvironment::<ApiV3>::build_setup_api(&db).await;
|
||||
dummy_data::add_dummy_data(&setup_api, db.clone()).await;
|
||||
db.pool.close().await;
|
||||
}
|
||||
pool.close().await;
|
||||
drop(pool);
|
||||
|
||||
// Create the temporary database from the template
|
||||
let create_db_query = format!(
|
||||
"CREATE DATABASE {} TEMPLATE {}",
|
||||
&temp_database_name, TEMPLATE_DATABASE_NAME
|
||||
);
|
||||
|
||||
sqlx::query(&create_db_query)
|
||||
.execute(&main_pool)
|
||||
.await
|
||||
.expect("Database creation failed");
|
||||
|
||||
// Release the advisory lock
|
||||
sqlx::query("SELECT pg_advisory_unlock(1)")
|
||||
.execute(&main_pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
main_pool.close().await;
|
||||
break;
|
||||
}
|
||||
// Wait for the lock to be released
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
||||
}
|
||||
}
|
||||
|
||||
// Deletes the temporary database (panics)
|
||||
// If a temporary db is created, it must be cleaned up with cleanup.
|
||||
// This means that dbs will only 'remain' if a test fails (for examination of the db), and will be cleaned up otherwise.
|
||||
pub async fn cleanup(mut self) {
|
||||
let database_url = dotenvy::var("DATABASE_URL").expect("No database URL");
|
||||
self.pool.close().await;
|
||||
|
||||
self.pool = PgPool::connect(&database_url)
|
||||
.await
|
||||
.expect("Connection to main database failed");
|
||||
|
||||
// Forcibly terminate all existing connections to this version of the temporary database
|
||||
// We are done and deleting it, so we don't need them anymore
|
||||
let terminate_query = format!(
|
||||
"SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE datname = '{}' AND pid <> pg_backend_pid()",
|
||||
&self.database_name
|
||||
);
|
||||
sqlx::query(&terminate_query)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Execute the deletion query asynchronously
|
||||
let drop_db_query = format!("DROP DATABASE IF EXISTS {}", &self.database_name);
|
||||
sqlx::query(&drop_db_query)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.expect("Database deletion failed");
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_template_database(pool: &sqlx::Pool<sqlx::Postgres>) {
|
||||
let create_db_query = format!("CREATE DATABASE {TEMPLATE_DATABASE_NAME}");
|
||||
sqlx::query(&create_db_query)
|
||||
.execute(pool)
|
||||
.await
|
||||
.expect("Database creation failed");
|
||||
}
|
||||
|
||||
// Appends a random 8-digit number to the end of the str
|
||||
pub fn generate_random_name(str: &str) -> String {
|
||||
let mut str = String::from(str);
|
||||
str.push_str(&rand::random::<u64>().to_string()[..8]);
|
||||
str
|
||||
}
|
||||
561
apps/labrinth/tests/common/dummy_data.rs
Normal file
561
apps/labrinth/tests/common/dummy_data.rs
Normal file
@@ -0,0 +1,561 @@
|
||||
#![allow(dead_code)]
|
||||
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,
|
||||
projects::{Project, ProjectId, Version},
|
||||
};
|
||||
use serde_json::json;
|
||||
use sqlx::Executor;
|
||||
use zip::{write::FileOptions, CompressionMethod, ZipWriter};
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::{api_common::Api, api_v3, database::USER_USER_PAT},
|
||||
};
|
||||
|
||||
use super::{
|
||||
api_common::{request_data::ImageData, ApiProject, AppendsOptionalPat},
|
||||
api_v3::ApiV3,
|
||||
database::TemporaryDatabase,
|
||||
};
|
||||
|
||||
use super::{database::USER_USER_ID, get_json_val_str};
|
||||
|
||||
pub const DUMMY_DATA_UPDATE: i64 = 7;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub const DUMMY_CATEGORIES: &[&str] = &[
|
||||
"combat",
|
||||
"decoration",
|
||||
"economy",
|
||||
"food",
|
||||
"magic",
|
||||
"mobs",
|
||||
"optimization",
|
||||
];
|
||||
|
||||
pub const DUMMY_OAUTH_CLIENT_ALPHA_SECRET: &str = "abcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone)]
|
||||
pub enum TestFile {
|
||||
DummyProjectAlpha,
|
||||
DummyProjectBeta,
|
||||
BasicZip,
|
||||
BasicMod,
|
||||
BasicModDifferent,
|
||||
// Randomly generates a valid .jar with a random hash.
|
||||
// Unlike the other dummy jar files, this one is not a static file.
|
||||
// and BasicModRandom.bytes() will return a different file each time.
|
||||
BasicModRandom { filename: String, bytes: Vec<u8> },
|
||||
BasicModpackRandom { filename: String, bytes: Vec<u8> },
|
||||
}
|
||||
|
||||
impl TestFile {
|
||||
pub fn build_random_jar() -> Self {
|
||||
let filename = format!("random-mod-{}.jar", rand::random::<u64>());
|
||||
|
||||
let fabric_mod_json = serde_json::json!({
|
||||
"schemaVersion": 1,
|
||||
"id": filename,
|
||||
"version": "1.0.1",
|
||||
|
||||
"name": filename,
|
||||
"description": "Does nothing",
|
||||
"authors": [
|
||||
"user"
|
||||
],
|
||||
"contact": {
|
||||
"homepage": "https://www.modrinth.com",
|
||||
"sources": "https://www.modrinth.com",
|
||||
"issues": "https://www.modrinth.com"
|
||||
},
|
||||
|
||||
"license": "MIT",
|
||||
"icon": "none.png",
|
||||
|
||||
"environment": "client",
|
||||
"entrypoints": {
|
||||
"main": [
|
||||
"io.github.modrinth.Modrinth"
|
||||
]
|
||||
},
|
||||
"depends": {
|
||||
"minecraft": ">=1.20-"
|
||||
}
|
||||
}
|
||||
)
|
||||
.to_string();
|
||||
|
||||
// Create a simulated zip file
|
||||
let mut cursor = Cursor::new(Vec::new());
|
||||
{
|
||||
let mut zip = ZipWriter::new(&mut cursor);
|
||||
zip.start_file(
|
||||
"fabric.mod.json",
|
||||
FileOptions::default().compression_method(CompressionMethod::Stored),
|
||||
)
|
||||
.unwrap();
|
||||
zip.write_all(fabric_mod_json.as_bytes()).unwrap();
|
||||
|
||||
zip.start_file(
|
||||
"META-INF/mods.toml",
|
||||
FileOptions::default().compression_method(CompressionMethod::Stored),
|
||||
)
|
||||
.unwrap();
|
||||
zip.write_all(fabric_mod_json.as_bytes()).unwrap();
|
||||
|
||||
zip.finish().unwrap();
|
||||
}
|
||||
let bytes = cursor.into_inner();
|
||||
|
||||
TestFile::BasicModRandom { filename, bytes }
|
||||
}
|
||||
|
||||
pub fn build_random_mrpack() -> Self {
|
||||
let filename = format!("random-modpack-{}.mrpack", rand::random::<u64>());
|
||||
|
||||
let modrinth_index_json = serde_json::json!({
|
||||
"formatVersion": 1,
|
||||
"game": "minecraft",
|
||||
"versionId": "1.20.1-9.6",
|
||||
"name": filename,
|
||||
"files": [
|
||||
{
|
||||
"path": "mods/animatica-0.6+1.20.jar",
|
||||
"hashes": {
|
||||
"sha1": "3bcb19c759f313e69d3f7848b03c48f15167b88d",
|
||||
"sha512": "7d50f3f34479f8b052bfb9e2482603b4906b8984039777dc2513ecf18e9af2b599c9d094e88cec774f8525345859e721a394c8cd7c14a789c9538d2533c71d65"
|
||||
},
|
||||
"env": {
|
||||
"client": "required",
|
||||
"server": "required"
|
||||
},
|
||||
"downloads": [
|
||||
"https://cdn.modrinth.com/data/PRN43VSY/versions/uNgEPb10/animatica-0.6%2B1.20.jar"
|
||||
],
|
||||
"fileSize": 69810
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"fabric-loader": "0.14.22",
|
||||
"minecraft": "1.20.1"
|
||||
}
|
||||
}
|
||||
)
|
||||
.to_string();
|
||||
|
||||
// Create a simulated zip file
|
||||
let mut cursor = Cursor::new(Vec::new());
|
||||
{
|
||||
let mut zip = ZipWriter::new(&mut cursor);
|
||||
zip.start_file(
|
||||
"modrinth.index.json",
|
||||
FileOptions::default().compression_method(CompressionMethod::Stored),
|
||||
)
|
||||
.unwrap();
|
||||
zip.write_all(modrinth_index_json.as_bytes()).unwrap();
|
||||
zip.finish().unwrap();
|
||||
}
|
||||
let bytes = cursor.into_inner();
|
||||
|
||||
TestFile::BasicModpackRandom { filename, bytes }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(dead_code)]
|
||||
pub enum DummyImage {
|
||||
SmallIcon, // 200x200
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DummyData {
|
||||
/// Alpha project:
|
||||
/// This is a dummy project created by USER user.
|
||||
/// It's approved, listed, and visible to the public.
|
||||
pub project_alpha: DummyProjectAlpha,
|
||||
|
||||
/// Beta project:
|
||||
/// This is a dummy project created by USER user.
|
||||
/// It's not approved, unlisted, and not visible to the public.
|
||||
pub project_beta: DummyProjectBeta,
|
||||
|
||||
/// Zeta organization:
|
||||
/// This is a dummy organization created by USER user.
|
||||
/// There are no projects in it.
|
||||
pub organization_zeta: DummyOrganizationZeta,
|
||||
|
||||
/// Alpha OAuth Client:
|
||||
/// This is a dummy OAuth client created by USER user.
|
||||
///
|
||||
/// All scopes are included in its max scopes
|
||||
///
|
||||
/// It has one valid redirect URI
|
||||
pub oauth_client_alpha: DummyOAuthClientAlpha,
|
||||
}
|
||||
|
||||
impl DummyData {
|
||||
pub fn new(
|
||||
project_alpha: Project,
|
||||
project_alpha_version: Version,
|
||||
project_beta: Project,
|
||||
project_beta_version: Version,
|
||||
organization_zeta: Organization,
|
||||
oauth_client_alpha: OAuthClient,
|
||||
) -> Self {
|
||||
DummyData {
|
||||
project_alpha: DummyProjectAlpha {
|
||||
team_id: project_alpha.team_id.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(),
|
||||
},
|
||||
|
||||
project_beta: DummyProjectBeta {
|
||||
team_id: project_beta.team_id.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(),
|
||||
},
|
||||
|
||||
organization_zeta: DummyOrganizationZeta {
|
||||
organization_id: organization_zeta.id.to_string(),
|
||||
team_id: organization_zeta.team_id.to_string(),
|
||||
organization_slug: organization_zeta.slug,
|
||||
},
|
||||
|
||||
oauth_client_alpha: DummyOAuthClientAlpha {
|
||||
client_id: get_json_val_str(oauth_client_alpha.id),
|
||||
client_secret: DUMMY_OAUTH_CLIENT_ALPHA_SECRET.to_string(),
|
||||
valid_redirect_uri: oauth_client_alpha
|
||||
.redirect_uris
|
||||
.first()
|
||||
.unwrap()
|
||||
.uri
|
||||
.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
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,
|
||||
pub team_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
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,
|
||||
pub team_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DummyOrganizationZeta {
|
||||
pub organization_id: String,
|
||||
pub organization_slug: String,
|
||||
pub team_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DummyOAuthClientAlpha {
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
pub valid_redirect_uri: String,
|
||||
}
|
||||
|
||||
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 = &db.pool.clone();
|
||||
|
||||
pool.execute(
|
||||
include_str!("../files/dummy_data.sql")
|
||||
.replace("$1", &Scopes::all().bits().to_string())
|
||||
.as_str(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
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(api).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)
|
||||
.execute(pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
DummyData::new(
|
||||
alpha_project,
|
||||
alpha_version,
|
||||
beta_project,
|
||||
beta_version,
|
||||
zeta_organization,
|
||||
oauth_client_alpha,
|
||||
)
|
||||
}
|
||||
|
||||
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(api).await;
|
||||
|
||||
let oauth_client_alpha = get_oauth_client_alpha(api).await;
|
||||
|
||||
DummyData::new(
|
||||
alpha_project,
|
||||
alpha_version,
|
||||
beta_project,
|
||||
beta_version,
|
||||
zeta_organization,
|
||||
oauth_client_alpha,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn add_project_alpha(api: &ApiV3) -> (Project, Version) {
|
||||
let (project, versions) = api
|
||||
.add_public_project(
|
||||
"alpha",
|
||||
Some(TestFile::DummyProjectAlpha),
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
let alpha_project = api
|
||||
.get_project_deserialized(project.id.to_string().as_str(), USER_USER_PAT)
|
||||
.await;
|
||||
let alpha_version = api
|
||||
.get_version_deserialized(
|
||||
&versions.into_iter().next().unwrap().id.to_string(),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
(alpha_project, alpha_version)
|
||||
}
|
||||
|
||||
pub async fn add_project_beta(api: &ApiV3) -> (Project, Version) {
|
||||
// Adds dummy data to the database with sqlx (projects, versions, threads)
|
||||
// Generate test project data.
|
||||
let jar = TestFile::DummyProjectBeta;
|
||||
// TODO: this shouldnt be hardcoded (nor should other similar ones be)
|
||||
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/summary", "value": "A dummy project for testing with." },
|
||||
{ "op": "add", "path": "/description", "value": "This project is not-yet-approved, and versions are draft." },
|
||||
{ "op": "add", "path": "/initial_versions/0/status", "value": "unlisted" },
|
||||
{ "op": "add", "path": "/status", "value": "private" },
|
||||
{ "op": "add", "path": "/requested_status", "value": "private" },
|
||||
]))
|
||||
.unwrap();
|
||||
|
||||
let creation_data = api_v3::request_data::get_public_project_creation_data(
|
||||
"beta",
|
||||
Some(jar),
|
||||
Some(modify_json),
|
||||
);
|
||||
api.create_project(creation_data, USER_USER_PAT).await;
|
||||
|
||||
get_project_beta(api).await
|
||||
}
|
||||
|
||||
pub async fn add_organization_zeta(api: &ApiV3) -> Organization {
|
||||
// Add an organzation.
|
||||
let req = TestRequest::post()
|
||||
.uri("/v3/organization")
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"name": "Zeta",
|
||||
"slug": "zeta",
|
||||
"description": "A dummy organization for testing with."
|
||||
}))
|
||||
.to_request();
|
||||
let resp = api.call(req).await;
|
||||
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
get_organization_zeta(api).await
|
||||
}
|
||||
|
||||
pub async fn get_project_alpha(api: &ApiV3) -> (Project, Version) {
|
||||
// Get project
|
||||
let req = TestRequest::get()
|
||||
.uri("/v3/project/alpha")
|
||||
.append_pat(USER_USER_PAT)
|
||||
.to_request();
|
||||
let resp = api.call(req).await;
|
||||
let project: Project = test::read_body_json(resp).await;
|
||||
|
||||
// Get project's versions
|
||||
let req = TestRequest::get()
|
||||
.uri("/v3/project/alpha/version")
|
||||
.append_pat(USER_USER_PAT)
|
||||
.to_request();
|
||||
let resp = api.call(req).await;
|
||||
let versions: Vec<Version> = test::read_body_json(resp).await;
|
||||
let version = versions.into_iter().next().unwrap();
|
||||
|
||||
(project, version)
|
||||
}
|
||||
|
||||
pub async fn get_project_beta(api: &ApiV3) -> (Project, Version) {
|
||||
// Get project
|
||||
let req = TestRequest::get()
|
||||
.uri("/v3/project/beta")
|
||||
.append_pat(USER_USER_PAT)
|
||||
.to_request();
|
||||
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();
|
||||
|
||||
// Get project's versions
|
||||
let req = TestRequest::get()
|
||||
.uri("/v3/project/beta/version")
|
||||
.append_pat(USER_USER_PAT)
|
||||
.to_request();
|
||||
let resp = api.call(req).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
let versions: Vec<Version> = test::read_body_json(resp).await;
|
||||
let version = versions.into_iter().next().unwrap();
|
||||
|
||||
(project, version)
|
||||
}
|
||||
|
||||
pub async fn get_organization_zeta(api: &ApiV3) -> Organization {
|
||||
// Get organization
|
||||
let req = TestRequest::get()
|
||||
.uri("/v3/organization/zeta")
|
||||
.append_pat(USER_USER_PAT)
|
||||
.to_request();
|
||||
let resp = api.call(req).await;
|
||||
let organization: Organization = test::read_body_json(resp).await;
|
||||
|
||||
organization
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
impl TestFile {
|
||||
pub fn filename(&self) -> String {
|
||||
match self {
|
||||
TestFile::DummyProjectAlpha => "dummy-project-alpha.jar",
|
||||
TestFile::DummyProjectBeta => "dummy-project-beta.jar",
|
||||
TestFile::BasicZip => "simple-zip.zip",
|
||||
TestFile::BasicMod => "basic-mod.jar",
|
||||
TestFile::BasicModDifferent => "basic-mod-different.jar",
|
||||
TestFile::BasicModRandom { filename, .. } => filename,
|
||||
TestFile::BasicModpackRandom { filename, .. } => filename,
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn bytes(&self) -> Vec<u8> {
|
||||
match self {
|
||||
TestFile::DummyProjectAlpha => {
|
||||
include_bytes!("../../tests/files/dummy-project-alpha.jar").to_vec()
|
||||
}
|
||||
TestFile::DummyProjectBeta => {
|
||||
include_bytes!("../../tests/files/dummy-project-beta.jar").to_vec()
|
||||
}
|
||||
TestFile::BasicMod => include_bytes!("../../tests/files/basic-mod.jar").to_vec(),
|
||||
TestFile::BasicZip => include_bytes!("../../tests/files/simple-zip.zip").to_vec(),
|
||||
TestFile::BasicModDifferent => {
|
||||
include_bytes!("../../tests/files/basic-mod-different.jar").to_vec()
|
||||
}
|
||||
TestFile::BasicModRandom { bytes, .. } => bytes.clone(),
|
||||
TestFile::BasicModpackRandom { bytes, .. } => bytes.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn project_type(&self) -> String {
|
||||
match self {
|
||||
TestFile::DummyProjectAlpha => "mod",
|
||||
TestFile::DummyProjectBeta => "mod",
|
||||
TestFile::BasicMod => "mod",
|
||||
TestFile::BasicModDifferent => "mod",
|
||||
TestFile::BasicModRandom { .. } => "mod",
|
||||
|
||||
TestFile::BasicZip => "resourcepack",
|
||||
|
||||
TestFile::BasicModpackRandom { .. } => "modpack",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn content_type(&self) -> Option<String> {
|
||||
match self {
|
||||
TestFile::DummyProjectAlpha => Some("application/java-archive"),
|
||||
TestFile::DummyProjectBeta => Some("application/java-archive"),
|
||||
TestFile::BasicMod => Some("application/java-archive"),
|
||||
TestFile::BasicModDifferent => Some("application/java-archive"),
|
||||
TestFile::BasicModRandom { .. } => Some("application/java-archive"),
|
||||
|
||||
TestFile::BasicZip => Some("application/zip"),
|
||||
|
||||
TestFile::BasicModpackRandom { .. } => Some("application/x-modrinth-modpack+zip"),
|
||||
}
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl DummyImage {
|
||||
pub fn filename(&self) -> String {
|
||||
match self {
|
||||
DummyImage::SmallIcon => "200x200.png",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn extension(&self) -> String {
|
||||
match self {
|
||||
DummyImage::SmallIcon => "png",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn bytes(&self) -> Vec<u8> {
|
||||
match self {
|
||||
DummyImage::SmallIcon => include_bytes!("../../tests/files/200x200.png").to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_icon_data(&self) -> ImageData {
|
||||
ImageData {
|
||||
filename: self.filename(),
|
||||
extension: self.extension(),
|
||||
icon: self.bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
162
apps/labrinth/tests/common/environment.rs
Normal file
162
apps/labrinth/tests/common/environment.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use super::{
|
||||
api_common::{generic::GenericApi, Api, ApiBuildable},
|
||||
api_v2::ApiV2,
|
||||
api_v3::ApiV3,
|
||||
database::{TemporaryDatabase, FRIEND_USER_ID, USER_USER_PAT},
|
||||
dummy_data,
|
||||
};
|
||||
use crate::{assert_status, common::setup};
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::dev::ServiceResponse;
|
||||
use futures::Future;
|
||||
|
||||
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<A> = TestEnvironment::build(max_connections).await;
|
||||
let db = test_env.db.clone();
|
||||
f(test_env).await;
|
||||
db.cleanup().await;
|
||||
}
|
||||
|
||||
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<A> {
|
||||
pub db: TemporaryDatabase,
|
||||
pub api: A,
|
||||
pub setup_api: ApiV3, // Used for setting up tests only (ie: in ScopesTest)
|
||||
pub dummy: dummy_data::DummyData,
|
||||
}
|
||||
|
||||
impl<A: ApiBuildable> TestEnvironment<A> {
|
||||
async fn build(max_connections: Option<u32>) -> Self {
|
||||
let db = TemporaryDatabase::create(max_connections).await;
|
||||
let labrinth_config = setup(&db).await;
|
||||
let api = A::build(labrinth_config.clone()).await;
|
||||
let setup_api = ApiV3::build(labrinth_config).await;
|
||||
let dummy = dummy_data::get_dummy_data(&setup_api).await;
|
||||
Self {
|
||||
db,
|
||||
api,
|
||||
setup_api,
|
||||
dummy,
|
||||
}
|
||||
}
|
||||
pub async fn build_setup_api(db: &TemporaryDatabase) -> ApiV3 {
|
||||
let labrinth_config = setup(db).await;
|
||||
ApiV3::build(labrinth_config).await
|
||||
}
|
||||
}
|
||||
|
||||
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.api.call(req).await
|
||||
}
|
||||
|
||||
// Setup data, create a friend user notification
|
||||
pub async fn generate_friend_user_notification(&self) {
|
||||
let resp = self
|
||||
.api
|
||||
.add_user_to_team(
|
||||
&self.dummy.project_alpha.team_id,
|
||||
FRIEND_USER_ID,
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
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: Option<&str>,
|
||||
status_code: StatusCode,
|
||||
) {
|
||||
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: Option<&str>,
|
||||
status_code: StatusCode,
|
||||
) {
|
||||
let resp = self.api.get_user_projects(user_id, pat).await;
|
||||
assert_status!(&resp, status_code);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LocalService {
|
||||
fn call(
|
||||
&self,
|
||||
req: actix_http::Request,
|
||||
) -> std::pin::Pin<
|
||||
Box<dyn std::future::Future<Output = Result<ServiceResponse, actix_web::Error>>>,
|
||||
>;
|
||||
}
|
||||
impl<S> LocalService for S
|
||||
where
|
||||
S: actix_web::dev::Service<
|
||||
actix_http::Request,
|
||||
Response = ServiceResponse,
|
||||
Error = actix_web::Error,
|
||||
>,
|
||||
S::Future: 'static,
|
||||
{
|
||||
fn call(
|
||||
&self,
|
||||
req: actix_http::Request,
|
||||
) -> std::pin::Pin<
|
||||
Box<dyn std::future::Future<Output = Result<ServiceResponse, actix_web::Error>>>,
|
||||
> {
|
||||
Box::pin(self.call(req))
|
||||
}
|
||||
}
|
||||
53
apps/labrinth/tests/common/mod.rs
Normal file
53
apps/labrinth/tests/common/mod.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
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;
|
||||
pub mod database;
|
||||
pub mod dummy_data;
|
||||
pub mod environment;
|
||||
pub mod pats;
|
||||
pub mod permissions;
|
||||
pub mod scopes;
|
||||
pub mod search;
|
||||
|
||||
// Testing equivalent to 'setup' function, producing a LabrinthConfig
|
||||
// If making a test, you should probably use environment::TestEnvironment::build() (which calls this)
|
||||
pub async fn setup(db: &database::TemporaryDatabase) -> LabrinthConfig {
|
||||
println!("Setting up labrinth config");
|
||||
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
if check_env_vars() {
|
||||
println!("Some environment variables are missing!");
|
||||
}
|
||||
|
||||
let pool = db.pool.clone();
|
||||
let redis_pool = db.redis_pool.clone();
|
||||
let search_config = db.search_config.clone();
|
||||
let file_host: Arc<dyn file_hosting::FileHost + Send + Sync> =
|
||||
Arc::new(file_hosting::MockHost::new());
|
||||
let mut clickhouse = clickhouse::init_client().await.unwrap();
|
||||
|
||||
let maxmind_reader = Arc::new(queue::maxmind::MaxMindIndexer::new().await.unwrap());
|
||||
|
||||
labrinth::app_setup(
|
||||
pool.clone(),
|
||||
redis_pool.clone(),
|
||||
search_config,
|
||||
&mut clickhouse,
|
||||
file_host.clone(),
|
||||
maxmind_reader,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_json_val_str(val: impl serde::Serialize) -> String {
|
||||
serde_json::to_value(val)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
30
apps/labrinth/tests/common/pats.rs
Normal file
30
apps/labrinth/tests/common/pats.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use chrono::Utc;
|
||||
use labrinth::{
|
||||
database::{self, models::generate_pat_id},
|
||||
models::pats::Scopes,
|
||||
};
|
||||
|
||||
use super::database::TemporaryDatabase;
|
||||
|
||||
// Creates a PAT with the given scopes, and returns the access token
|
||||
// Interfacing with the db directly, rather than using a ourte,
|
||||
// allows us to test with scopes that are not allowed to be created by PATs
|
||||
pub async fn create_test_pat(scopes: Scopes, user_id: i64, db: &TemporaryDatabase) -> String {
|
||||
let mut transaction = db.pool.begin().await.unwrap();
|
||||
let id = generate_pat_id(&mut transaction).await.unwrap();
|
||||
let pat = database::models::pat_item::PersonalAccessToken {
|
||||
id,
|
||||
name: format!("test_pat_{}", scopes.bits()),
|
||||
access_token: format!("mrp_{}", id.0),
|
||||
scopes,
|
||||
user_id: database::models::ids::UserId(user_id),
|
||||
created: Utc::now(),
|
||||
expires: Utc::now() + chrono::Duration::days(1),
|
||||
last_used: None,
|
||||
};
|
||||
pat.insert(&mut transaction).await.unwrap();
|
||||
transaction.commit().await.unwrap();
|
||||
pat.access_token
|
||||
}
|
||||
1161
apps/labrinth/tests/common/permissions.rs
Normal file
1161
apps/labrinth/tests/common/permissions.rs
Normal file
File diff suppressed because it is too large
Load Diff
123
apps/labrinth/tests/common/scopes.rs
Normal file
123
apps/labrinth/tests/common/scopes.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
#![allow(dead_code)]
|
||||
use actix_web::{dev::ServiceResponse, test};
|
||||
use futures::Future;
|
||||
use labrinth::models::pats::Scopes;
|
||||
|
||||
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, 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>,
|
||||
// User ID to use for the PATs. By default, this is the USER_USER_ID_PARSED constant.
|
||||
user_id: i64,
|
||||
// The code that is expected to be returned if the scope is not present. By default, this is 401 (Unauthorized)
|
||||
expected_failure_code: u16,
|
||||
}
|
||||
|
||||
impl<'a, A: Api> ScopeTest<'a, A> {
|
||||
pub fn new(test_env: &'a TestEnvironment<A>) -> Self {
|
||||
Self {
|
||||
test_env,
|
||||
failure_scopes: None,
|
||||
user_id: USER_USER_ID_PARSED,
|
||||
expected_failure_code: 401,
|
||||
}
|
||||
}
|
||||
|
||||
// Set non-standard failure scopes
|
||||
// If not set, it will be set to all scopes except the success scopes
|
||||
// (eg: if a combination of scopes is needed, but you want to make sure that the endpoint does not work with all-but-one of them)
|
||||
pub fn with_failure_scopes(mut self, scopes: Scopes) -> Self {
|
||||
self.failure_scopes = Some(scopes);
|
||||
self
|
||||
}
|
||||
|
||||
// Set the user ID to use
|
||||
// (eg: a moderator, or friend)
|
||||
pub fn with_user_id(mut self, user_id: i64) -> Self {
|
||||
self.user_id = user_id;
|
||||
self
|
||||
}
|
||||
|
||||
// If a non-401 code is expected.
|
||||
// (eg: a 404 for a hidden resource, or 200 for a resource with hidden values deeper in)
|
||||
pub fn with_failure_code(mut self, code: u16) -> Self {
|
||||
self.expected_failure_code = code;
|
||||
self
|
||||
}
|
||||
|
||||
// Call the endpoint generated by req_gen twice, once with a PAT with the failure scopes, and once with the success scopes.
|
||||
// success_scopes : the scopes that we are testing that should succeed
|
||||
// returns a tuple of (failure_body, success_body)
|
||||
// Should return a String error if on unexpected status code, allowing unwrapping in tests.
|
||||
pub async fn test<T, Fut>(
|
||||
&self,
|
||||
req_gen: T,
|
||||
success_scopes: Scopes,
|
||||
) -> Result<(serde_json::Value, serde_json::Value), String>
|
||||
where
|
||||
T: Fn(Option<String>) -> Fut,
|
||||
Fut: Future<Output = ServiceResponse>, // Ensure Fut is Send and 'static
|
||||
{
|
||||
// First, create a PAT with failure scopes
|
||||
let failure_scopes = self
|
||||
.failure_scopes
|
||||
.unwrap_or(Scopes::all() ^ success_scopes);
|
||||
let access_token_all_others =
|
||||
create_test_pat(failure_scopes, self.user_id, &self.test_env.db).await;
|
||||
|
||||
// Create a PAT with the success scopes
|
||||
let access_token = create_test_pat(success_scopes, self.user_id, &self.test_env.db).await;
|
||||
|
||||
// Perform test twice, once with each PAT
|
||||
// the first time, we expect a 401 (or known failure code)
|
||||
let resp = req_gen(Some(access_token_all_others.clone())).await;
|
||||
if resp.status().as_u16() != self.expected_failure_code {
|
||||
return Err(format!(
|
||||
"Expected failure code {}, got {} ({:#?})",
|
||||
self.expected_failure_code,
|
||||
resp.status().as_u16(),
|
||||
resp.response()
|
||||
));
|
||||
}
|
||||
|
||||
let failure_body = if resp.status() == 200
|
||||
&& resp.headers().contains_key("Content-Type")
|
||||
&& resp.headers().get("Content-Type").unwrap() == "application/json"
|
||||
{
|
||||
test::read_body_json(resp).await
|
||||
} else {
|
||||
serde_json::Value::Null
|
||||
};
|
||||
|
||||
// The second time, we expect a success code
|
||||
let resp = req_gen(Some(access_token.clone())).await;
|
||||
if !(resp.status().is_success() || resp.status().is_redirection()) {
|
||||
return Err(format!(
|
||||
"Expected success code, got {} ({:#?})",
|
||||
resp.status().as_u16(),
|
||||
resp.response()
|
||||
));
|
||||
}
|
||||
|
||||
let success_body = if resp.status() == 200
|
||||
&& resp.headers().contains_key("Content-Type")
|
||||
&& resp.headers().get("Content-Type").unwrap() == "application/json"
|
||||
{
|
||||
test::read_body_json(resp).await
|
||||
} else {
|
||||
serde_json::Value::Null
|
||||
};
|
||||
Ok((failure_body, success_body))
|
||||
}
|
||||
}
|
||||
234
apps/labrinth/tests/common/search.rs
Normal file
234
apps/labrinth/tests/common/search.rs
Normal file
@@ -0,0 +1,234 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use actix_http::StatusCode;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::{
|
||||
api_common::{Api, ApiProject, ApiVersion},
|
||||
database::{FRIEND_USER_PAT, MOD_USER_PAT, USER_USER_PAT},
|
||||
dummy_data::{TestFile, DUMMY_CATEGORIES},
|
||||
},
|
||||
};
|
||||
|
||||
use super::{api_v3::ApiV3, environment::TestEnvironment};
|
||||
|
||||
pub async fn setup_search_projects(test_env: &TestEnvironment<ApiV3>) -> Arc<HashMap<u64, u64>> {
|
||||
// Test setup and dummy data
|
||||
let api = &test_env.api;
|
||||
let test_name = test_env.db.database_name.clone();
|
||||
let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id;
|
||||
|
||||
// Add dummy projects of various categories for searchability
|
||||
let mut project_creation_futures = vec![];
|
||||
|
||||
let create_async_future =
|
||||
|id: u64,
|
||||
pat: Option<&'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()
|
||||
};
|
||||
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_status!(&resp, StatusCode::NO_CONTENT);
|
||||
(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_only", "value": true },
|
||||
{ "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
USER_USER_PAT,
|
||||
false,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Test project 1
|
||||
let id = 1;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] },
|
||||
{ "op": "add", "path": "/initial_versions/0/client_only", "value": false },
|
||||
]))
|
||||
.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 = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] },
|
||||
{ "op": "add", "path": "/initial_versions/0/server_only", "value": true },
|
||||
{ "op": "add", "path": "/name", "value": "Mysterious Project" },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
USER_USER_PAT,
|
||||
false,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Test project 3
|
||||
let id = 3;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] },
|
||||
{ "op": "add", "path": "/initial_versions/0/server_only", "value": true },
|
||||
{ "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.4"] },
|
||||
{ "op": "add", "path": "/name", "value": "Mysterious Project" },
|
||||
{ "op": "add", "path": "/license_id", "value": "LicenseRef-All-Rights-Reserved" },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
FRIEND_USER_PAT,
|
||||
false,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Test project 4
|
||||
let id = 4;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] },
|
||||
{ "op": "add", "path": "/initial_versions/0/client_only", "value": false },
|
||||
{ "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.5"] },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
USER_USER_PAT,
|
||||
true,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Test project 5
|
||||
let id = 5;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] },
|
||||
{ "op": "add", "path": "/initial_versions/0/client_only", "value": false },
|
||||
{ "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.5"] },
|
||||
{ "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
USER_USER_PAT,
|
||||
false,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Test project 6
|
||||
let id = 6;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] },
|
||||
{ "op": "add", "path": "/initial_versions/0/client_only", "value": false },
|
||||
{ "op": "add", "path": "/initial_versions/0/server_only", "value": true },
|
||||
{ "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
FRIEND_USER_PAT,
|
||||
false,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Test project 7 (testing the search bug)
|
||||
// This project has an initial private forge version that is 1.20.3, and a fabric 1.20.5 version.
|
||||
// This means that a search for fabric + 1.20.3 or forge + 1.20.5 should not return this project.
|
||||
let id = 7;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] },
|
||||
{ "op": "add", "path": "/initial_versions/0/client_only", "value": false },
|
||||
{ "op": "add", "path": "/initial_versions/0/server_only", "value": true },
|
||||
{ "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 9 (organization)
|
||||
// This project gets added to the Zeta organization automatically
|
||||
let id = 9;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/organization_id", "value": zeta_organization_id },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
USER_USER_PAT,
|
||||
false,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Await all project creation
|
||||
// Returns a mapping of:
|
||||
// project id -> test id
|
||||
let id_conversion: Arc<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_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,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Forcibly reset the search index
|
||||
let resp = api.reset_search_index().await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
id_conversion
|
||||
}
|
||||
24
apps/labrinth/tests/error.rs
Normal file
24
apps/labrinth/tests/error.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::test;
|
||||
use bytes::Bytes;
|
||||
use common::api_common::ApiProject;
|
||||
|
||||
use common::api_v3::ApiV3;
|
||||
use common::database::USER_USER_PAT;
|
||||
use common::environment::{with_test_environment, TestEnvironment};
|
||||
|
||||
mod common;
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn error_404_body() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
// 3 errors should have 404 as non-blank body, for missing resources
|
||||
let api = &test_env.api;
|
||||
let resp = api.get_project("does-not-exist", USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NOT_FOUND);
|
||||
let body = test::read_body(resp).await;
|
||||
let empty_bytes = Bytes::from_static(b"");
|
||||
assert_ne!(body, empty_bytes);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
BIN
apps/labrinth/tests/files/200x200.png
Normal file
BIN
apps/labrinth/tests/files/200x200.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 606 B |
BIN
apps/labrinth/tests/files/basic-mod-different.jar
Normal file
BIN
apps/labrinth/tests/files/basic-mod-different.jar
Normal file
Binary file not shown.
BIN
apps/labrinth/tests/files/basic-mod.jar
Normal file
BIN
apps/labrinth/tests/files/basic-mod.jar
Normal file
Binary file not shown.
BIN
apps/labrinth/tests/files/dummy-project-alpha.jar
Normal file
BIN
apps/labrinth/tests/files/dummy-project-alpha.jar
Normal file
Binary file not shown.
BIN
apps/labrinth/tests/files/dummy-project-beta.jar
Normal file
BIN
apps/labrinth/tests/files/dummy-project-beta.jar
Normal file
Binary file not shown.
113
apps/labrinth/tests/files/dummy_data.sql
Normal file
113
apps/labrinth/tests/files/dummy_data.sql
Normal file
@@ -0,0 +1,113 @@
|
||||
-- Dummy test data for use in tests.
|
||||
-- IDs are listed as integers, followed by their equivalent base 62 representation.
|
||||
|
||||
-- Inserts 5 dummy users for testing, with slight differences
|
||||
-- 'Friend' and 'enemy' function like 'user', but we can use them to simulate 'other' users that may or may not be able to access certain things
|
||||
-- IDs 1-5, 1-5
|
||||
INSERT INTO users (id, username, email, role) VALUES (1, 'Admin', 'admin@modrinth.com', 'admin');
|
||||
INSERT INTO users (id, username, email, role) VALUES (2, 'Moderator', 'moderator@modrinth.com', 'moderator');
|
||||
INSERT INTO users (id, username, email, role) VALUES (3, 'User', 'user@modrinth.com', 'developer');
|
||||
INSERT INTO users (id, username, email, role) VALUES (4, 'Friend', 'friend@modrinth.com', 'developer');
|
||||
INSERT INTO users (id, username, email, role) VALUES (5, 'Enemy', 'enemy@modrinth.com', 'developer');
|
||||
|
||||
-- Full PATs for each user, with different scopes
|
||||
-- These are not legal PATs, as they contain all scopes- they mimic permissions of a logged in user
|
||||
-- IDs: 50-54, o p q r s
|
||||
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (50, 1, 'admin-pat', 'mrp_patadmin', $1, '2030-08-18 15:48:58.435729+00');
|
||||
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (51, 2, 'moderator-pat', 'mrp_patmoderator', $1, '2030-08-18 15:48:58.435729+00');
|
||||
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (52, 3, 'user-pat', 'mrp_patuser', $1, '2030-08-18 15:48:58.435729+00');
|
||||
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (53, 4, 'friend-pat', 'mrp_patfriend', $1, '2030-08-18 15:48:58.435729+00');
|
||||
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (54, 5, 'enemy-pat', 'mrp_patenemy', $1, '2030-08-18 15:48:58.435729+00');
|
||||
|
||||
INSERT INTO loaders (id, loader) VALUES (5, 'fabric');
|
||||
INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id) VALUES (5,1);
|
||||
|
||||
INSERT INTO loaders (id, loader) VALUES (6, 'forge');
|
||||
INSERT INTO loaders_project_types (joining_loader_id, joining_project_type_id) VALUES (6,1);
|
||||
|
||||
INSERT INTO loaders (id, loader, metadata) VALUES (7, 'bukkit', '{"platform":false}'::jsonb);
|
||||
INSERT INTO loaders (id, loader, metadata) VALUES (8, 'waterfall', '{"platform":true}'::jsonb);
|
||||
|
||||
-- Adds dummies to mrpack_loaders
|
||||
INSERT INTO loader_field_enum_values (enum_id, value) SELECT id, 'fabric' FROM loader_field_enums WHERE enum_name = 'mrpack_loaders';
|
||||
INSERT INTO loader_field_enum_values (enum_id, value) SELECT id, 'forge' FROM loader_field_enums WHERE enum_name = 'mrpack_loaders';
|
||||
|
||||
INSERT INTO loaders_project_types_games (loader_id, project_type_id, game_id) SELECT joining_loader_id, joining_project_type_id, 1 FROM loaders_project_types WHERE joining_loader_id = 5;
|
||||
INSERT INTO loaders_project_types_games (loader_id, project_type_id, game_id) SELECT joining_loader_id, joining_project_type_id, 1 FROM loaders_project_types WHERE joining_loader_id = 6;
|
||||
|
||||
-- Dummy-data only optional field, as we don't have any yet
|
||||
INSERT INTO loader_fields (
|
||||
field,
|
||||
field_type,
|
||||
optional
|
||||
) VALUES (
|
||||
'test_fabric_optional',
|
||||
'integer',
|
||||
true
|
||||
);
|
||||
INSERT INTO loader_fields_loaders(loader_id, loader_field_id)
|
||||
SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field = 'test_fabric_optional' AND l.loader = 'fabric' ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Sample game versions, loaders, categories
|
||||
-- Game versions is '2'
|
||||
INSERT INTO loader_field_enum_values(enum_id, value, metadata, created)
|
||||
VALUES (2, '1.20.1', '{"type":"release","major":false}', '2021-08-18 15:48:58.435729+00');
|
||||
INSERT INTO loader_field_enum_values(enum_id, value, metadata, created)
|
||||
VALUES (2, '1.20.2', '{"type":"release","major":false}', '2021-08-18 15:48:59.435729+00');
|
||||
INSERT INTO loader_field_enum_values(enum_id, value, metadata, created)
|
||||
VALUES (2, '1.20.3', '{"type":"release","major":false}', '2021-08-18 15:49:00.435729+00');
|
||||
INSERT INTO loader_field_enum_values(enum_id, value, metadata, created)
|
||||
VALUES (2, '1.20.4', '{"type":"beta","major":false}', '2021-08-18 15:49:01.435729+00');
|
||||
INSERT INTO loader_field_enum_values(enum_id, value, metadata, created)
|
||||
VALUES (2, '1.20.5', '{"type":"release","major":true}', '2061-08-18 15:49:02.435729+00');
|
||||
|
||||
-- Also add 'Ordering_Negative1' and 'Ordering_Positive100' to game versions (to test ordering override)
|
||||
INSERT INTO loader_field_enum_values(enum_id, value, metadata, ordering)
|
||||
VALUES (2, 'Ordering_Negative1', '{"type":"release","major":false}', -1);
|
||||
INSERT INTO loader_field_enum_values(enum_id, value, metadata, ordering)
|
||||
VALUES (2, 'Ordering_Positive100', '{"type":"release","major":false}', 100);
|
||||
|
||||
INSERT INTO loader_fields_loaders(loader_id, loader_field_id)
|
||||
SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf WHERE lf.field IN ('game_versions','singleplayer', 'client_and_server', 'client_only', 'server_only') ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO categories (id, category, project_type) VALUES
|
||||
(51, 'combat', 1),
|
||||
(52, 'decoration', 1),
|
||||
(53, 'economy', 1),
|
||||
(54, 'food', 1),
|
||||
(55, 'magic', 1),
|
||||
(56, 'mobs', 1),
|
||||
(57, 'optimization', 1);
|
||||
|
||||
INSERT INTO categories (id, category, project_type) VALUES
|
||||
(101, 'combat', 2),
|
||||
(102, 'decoration', 2),
|
||||
(103, 'economy', 2),
|
||||
(104, 'food', 2),
|
||||
(105, 'magic', 2),
|
||||
(106, 'mobs', 2),
|
||||
(107, 'optimization', 2);
|
||||
|
||||
-- Create dummy oauth client, secret_hash is SHA512 hash of full lowercase alphabet
|
||||
INSERT INTO oauth_clients (
|
||||
id,
|
||||
name,
|
||||
icon_url,
|
||||
max_scopes,
|
||||
secret_hash,
|
||||
created_by
|
||||
)
|
||||
VALUES (
|
||||
1,
|
||||
'oauth_client_alpha',
|
||||
NULL,
|
||||
$1,
|
||||
'4dbff86cc2ca1bae1e16468a05cb9881c97f1753bce3619034898faa1aabe429955a1bf8ec483d7421fe3c1646613a59ed5441fb0f321389f77f48a879c7b1f1',
|
||||
3
|
||||
);
|
||||
INSERT INTO oauth_client_redirect_uris (id, client_id, uri) VALUES (1, 1, 'https://modrinth.com/oauth_callback');
|
||||
|
||||
-- Create dummy data table to mark that this file has been run
|
||||
CREATE TABLE dummy_data (
|
||||
update_id bigint PRIMARY KEY
|
||||
);
|
||||
BIN
apps/labrinth/tests/files/simple-zip.zip
Normal file
BIN
apps/labrinth/tests/files/simple-zip.zip
Normal file
Binary file not shown.
26
apps/labrinth/tests/games.rs
Normal file
26
apps/labrinth/tests/games.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
// TODO: fold this into loader_fields.rs or tags.rs of other v3 testing PR
|
||||
|
||||
use common::{
|
||||
api_v3::ApiV3,
|
||||
environment::{with_test_environment, TestEnvironment},
|
||||
};
|
||||
|
||||
mod common;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn get_games() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let api = test_env.api;
|
||||
|
||||
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");
|
||||
|
||||
assert_eq!(games[0].slug, "minecraft-java");
|
||||
assert_eq!(games[1].slug, "minecraft-bedrock");
|
||||
})
|
||||
.await;
|
||||
}
|
||||
635
apps/labrinth/tests/loader_fields.rs
Normal file
635
apps/labrinth/tests/loader_fields.rs
Normal file
@@ -0,0 +1,635 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::test;
|
||||
use common::api_v3::ApiV3;
|
||||
use common::environment::{with_test_environment, TestEnvironment};
|
||||
use itertools::Itertools;
|
||||
use labrinth::database::models::legacy_loader_fields::MinecraftGameVersion;
|
||||
use labrinth::models::v3;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::common::api_common::{ApiProject, ApiVersion};
|
||||
use crate::common::api_v3::request_data::get_public_project_creation_data;
|
||||
use crate::common::database::*;
|
||||
|
||||
use crate::common::dummy_data::{DummyProjectAlpha, DummyProjectBeta, TestFile};
|
||||
|
||||
// importing common module.
|
||||
mod common;
|
||||
|
||||
#[actix_rt::test]
|
||||
|
||||
async fn creating_loader_fields() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let api = &test_env.api;
|
||||
let DummyProjectAlpha {
|
||||
project_id: alpha_project_id,
|
||||
project_id_parsed: alpha_project_id_parsed,
|
||||
version_id: alpha_version_id,
|
||||
..
|
||||
} = &test_env.dummy.project_alpha;
|
||||
let DummyProjectBeta {
|
||||
project_id_parsed: beta_project_id_parsed,
|
||||
..
|
||||
} = &test_env.dummy.project_beta;
|
||||
|
||||
// ALL THE FOLLOWING FOR CREATE AND PATCH
|
||||
// Cannot create a version with an extra argument that cannot be tied to a loader field ("invalid loader field")
|
||||
// TODO: - Create project
|
||||
// - Create version
|
||||
let resp = api
|
||||
.add_public_version(
|
||||
*alpha_project_id_parsed,
|
||||
"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_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
// - Patch
|
||||
let resp = api
|
||||
.edit_version(
|
||||
alpha_version_id,
|
||||
json!({
|
||||
"invalid": "invalid"
|
||||
}),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// 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_parsed,
|
||||
"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_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
// - Patch
|
||||
let resp = api
|
||||
.edit_version(
|
||||
alpha_version_id,
|
||||
json!({
|
||||
"mrpack_loaders": ["fabric"]
|
||||
}),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// 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_parsed,
|
||||
"1.0.0",
|
||||
TestFile::build_random_jar(),
|
||||
None,
|
||||
Some(
|
||||
serde_json::from_value(json!([{
|
||||
"op": "remove",
|
||||
"path": "/singleplayer"
|
||||
}]))
|
||||
.unwrap(),
|
||||
),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// 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_parsed,
|
||||
"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_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// 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_parsed,
|
||||
"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_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// - Patch
|
||||
let resp = api
|
||||
.edit_version(
|
||||
alpha_version_id,
|
||||
json!({
|
||||
"game_versions": []
|
||||
}),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// 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!(["singleplayer"]),
|
||||
] {
|
||||
// TODO: - Create project
|
||||
// - Create version
|
||||
let resp = api
|
||||
.add_public_version(
|
||||
*alpha_project_id_parsed,
|
||||
"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_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// - Patch
|
||||
let resp = api
|
||||
.edit_version(
|
||||
alpha_version_id,
|
||||
json!({
|
||||
"game_versions": bad_type_game_versions
|
||||
}),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 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_parsed,
|
||||
"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_status!(&resp, StatusCode::NO_CONTENT);
|
||||
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_parsed,
|
||||
"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": "/singleplayer",
|
||||
"value": false
|
||||
}, {
|
||||
"op": "add",
|
||||
"path": "/server_only",
|
||||
"value": true
|
||||
}]))
|
||||
.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("singleplayer").unwrap(), &json!(false));
|
||||
assert_eq!(v.fields.get("server_only").unwrap(), &json!(true));
|
||||
// - Patch
|
||||
let resp = api
|
||||
.edit_version(
|
||||
alpha_version_id,
|
||||
json!({
|
||||
"game_versions": ["1.20.1", "1.20.2"],
|
||||
"singleplayer": false,
|
||||
"server_only": true
|
||||
}),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
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"])
|
||||
);
|
||||
|
||||
// Now that we've created a version, we need to make sure that the Project's loader fields are updated (aggregate)
|
||||
// First, add a new version
|
||||
api.add_public_version_deserialized(
|
||||
*alpha_project_id_parsed,
|
||||
"1.0.1",
|
||||
TestFile::build_random_jar(),
|
||||
None,
|
||||
Some(
|
||||
serde_json::from_value(json!([{
|
||||
"op": "add",
|
||||
"path": "/game_versions",
|
||||
"value": ["1.20.5"]
|
||||
}, {
|
||||
"op": "add",
|
||||
"path": "/singleplayer",
|
||||
"value": false
|
||||
}]))
|
||||
.unwrap(),
|
||||
),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Also, add one to the beta project
|
||||
api.add_public_version_deserialized(
|
||||
*beta_project_id_parsed,
|
||||
"1.0.1",
|
||||
TestFile::build_random_jar(),
|
||||
None,
|
||||
Some(
|
||||
serde_json::from_value(json!([{
|
||||
"op": "add",
|
||||
"path": "/game_versions",
|
||||
"value": ["1.20.4"]
|
||||
}]))
|
||||
.unwrap(),
|
||||
),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = api
|
||||
.get_project_deserialized(&alpha_project_id.to_string(), USER_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(
|
||||
project.fields.get("game_versions").unwrap(),
|
||||
&[json!("1.20.1"), json!("1.20.2"), json!("1.20.5")]
|
||||
);
|
||||
assert!(project
|
||||
.fields
|
||||
.get("singleplayer")
|
||||
.unwrap()
|
||||
.contains(&json!(false)));
|
||||
assert!(project
|
||||
.fields
|
||||
.get("singleplayer")
|
||||
.unwrap()
|
||||
.contains(&json!(true)));
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn get_loader_fields_variants() {
|
||||
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;
|
||||
|
||||
// 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"
|
||||
]
|
||||
);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn get_available_loader_fields() {
|
||||
// Get available loader fields for a given loader
|
||||
// (ie: which fields are relevant for 'fabric', etc)
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let api = &test_env.api;
|
||||
let loaders = api.get_loaders_deserialized().await;
|
||||
|
||||
let fabric_loader_fields = loaders
|
||||
.iter()
|
||||
.find(|x| x.name == "fabric")
|
||||
.unwrap()
|
||||
.supported_fields
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
fabric_loader_fields,
|
||||
[
|
||||
"game_versions",
|
||||
"singleplayer",
|
||||
"client_and_server",
|
||||
"client_only",
|
||||
"server_only",
|
||||
"test_fabric_optional" // exists for testing
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
);
|
||||
|
||||
let mrpack_loader_fields = loaders
|
||||
.iter()
|
||||
.find(|x| x.name == "mrpack")
|
||||
.unwrap()
|
||||
.supported_fields
|
||||
.clone()
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
mrpack_loader_fields,
|
||||
[
|
||||
"game_versions",
|
||||
"singleplayer",
|
||||
"client_and_server",
|
||||
"client_only",
|
||||
"server_only",
|
||||
// mrpack has all the general fields as well as this
|
||||
"mrpack_loaders"
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_multi_get_redis_cache() {
|
||||
// Ensures a multi-project get including both modpacks and mods ddoes not
|
||||
// incorrectly cache loader fields
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let api = &test_env.api;
|
||||
|
||||
// Create 5 modpacks
|
||||
let mut modpacks = Vec::new();
|
||||
for i in 0..5 {
|
||||
let slug = format!("test-modpack-{}", i);
|
||||
|
||||
let creation_data = get_public_project_creation_data(
|
||||
&slug,
|
||||
Some(TestFile::build_random_mrpack()),
|
||||
None,
|
||||
);
|
||||
let resp = api.create_project(creation_data, USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
modpacks.push(slug);
|
||||
}
|
||||
|
||||
// Create 5 mods
|
||||
let mut mods = Vec::new();
|
||||
for i in 0..5 {
|
||||
let slug = format!("test-mod-{}", i);
|
||||
|
||||
let creation_data =
|
||||
get_public_project_creation_data(&slug, Some(TestFile::build_random_jar()), None);
|
||||
let resp = api.create_project(creation_data, USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
mods.push(slug);
|
||||
}
|
||||
|
||||
// Get all 10 projects
|
||||
let project_slugs = modpacks
|
||||
.iter()
|
||||
.map(|x| x.as_str())
|
||||
.chain(mods.iter().map(|x| x.as_str()))
|
||||
.collect_vec();
|
||||
let resp = api.get_projects(&project_slugs, USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
let projects: Vec<v3::projects::Project> = test::read_body_json(resp).await;
|
||||
assert_eq!(projects.len(), 10);
|
||||
|
||||
// Ensure all 5 modpacks have 'mrpack_loaders', and all 5 mods do not
|
||||
for project in projects.iter() {
|
||||
if modpacks.contains(project.slug.as_ref().unwrap()) {
|
||||
assert!(project.fields.contains_key("mrpack_loaders"));
|
||||
} else if mods.contains(project.slug.as_ref().unwrap()) {
|
||||
assert!(!project.fields.contains_key("mrpack_loaders"));
|
||||
} else {
|
||||
panic!("Unexpected project slug: {:?}", project.slug);
|
||||
}
|
||||
}
|
||||
|
||||
// Get a version from each project
|
||||
let version_ids_modpacks = projects
|
||||
.iter()
|
||||
.filter(|x| modpacks.contains(x.slug.as_ref().unwrap()))
|
||||
.map(|x| x.versions[0])
|
||||
.collect_vec();
|
||||
let version_ids_mods = projects
|
||||
.iter()
|
||||
.filter(|x| mods.contains(x.slug.as_ref().unwrap()))
|
||||
.map(|x| x.versions[0])
|
||||
.collect_vec();
|
||||
let version_ids = version_ids_modpacks
|
||||
.iter()
|
||||
.chain(version_ids_mods.iter())
|
||||
.map(|x| x.to_string())
|
||||
.collect_vec();
|
||||
let resp = api.get_versions(version_ids, USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
let versions: Vec<v3::projects::Version> = test::read_body_json(resp).await;
|
||||
assert_eq!(versions.len(), 10);
|
||||
|
||||
// Ensure all 5 versions from modpacks have 'mrpack_loaders', and all 5 versions from mods do not
|
||||
for version in versions.iter() {
|
||||
if version_ids_modpacks.contains(&version.id) {
|
||||
assert!(version.fields.contains_key("mrpack_loaders"));
|
||||
} else if version_ids_mods.contains(&version.id) {
|
||||
assert!(!version.fields.contains_key("mrpack_loaders"));
|
||||
} else {
|
||||
panic!("Unexpected version id: {:?}", version.id);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn minecraft_game_version_update() {
|
||||
// We simulate adding a Minecraft game version, to ensure other data doesn't get overwritten
|
||||
// This is basically a test for the insertion/concatenation query
|
||||
// This doesn't use a route (as this behaviour isn't exposed via a route, but a scheduled URL call)
|
||||
// We just interact with the labrinth functions directly
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let api = &test_env.api;
|
||||
|
||||
// First, get a list of all gameversions
|
||||
let game_versions = api
|
||||
.get_loader_field_variants_deserialized("game_versions")
|
||||
.await;
|
||||
|
||||
// A couple specific checks- in the dummy data, all game versions are marked as major=false except 1.20.5
|
||||
let name_to_major = game_versions
|
||||
.iter()
|
||||
.map(|x| {
|
||||
(
|
||||
x.value.clone(),
|
||||
x.metadata.get("major").unwrap().as_bool().unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<std::collections::HashMap<_, _>>();
|
||||
for (name, major) in name_to_major {
|
||||
if name == "1.20.5" {
|
||||
assert!(major);
|
||||
} else {
|
||||
assert!(!major);
|
||||
}
|
||||
}
|
||||
|
||||
// Now, we add a new game version, directly to the db
|
||||
let pool = test_env.db.pool.clone();
|
||||
let redis = test_env.db.redis_pool.clone();
|
||||
MinecraftGameVersion::builder()
|
||||
.version("1.20.6")
|
||||
.unwrap()
|
||||
.version_type("release")
|
||||
.unwrap()
|
||||
.created(
|
||||
// now
|
||||
&chrono::Utc::now(),
|
||||
)
|
||||
.insert(&pool, &redis)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Check again
|
||||
let game_versions = api
|
||||
.get_loader_field_variants_deserialized("game_versions")
|
||||
.await;
|
||||
|
||||
let name_to_major = game_versions
|
||||
.iter()
|
||||
.map(|x| {
|
||||
(
|
||||
x.value.clone(),
|
||||
x.metadata.get("major").unwrap().as_bool().unwrap(),
|
||||
)
|
||||
})
|
||||
.collect::<std::collections::HashMap<_, _>>();
|
||||
// Confirm that the new version is there
|
||||
assert!(name_to_major.contains_key("1.20.6"));
|
||||
// Confirm metadata is unaltered
|
||||
for (name, major) in name_to_major {
|
||||
if name == "1.20.5" {
|
||||
assert!(major);
|
||||
} else {
|
||||
assert!(!major);
|
||||
}
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
72
apps/labrinth/tests/notifications.rs
Normal file
72
apps/labrinth/tests/notifications.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use common::{
|
||||
database::{FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_PAT},
|
||||
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_all(None, |test_env| async move {
|
||||
let alpha_team_id = test_env.dummy.project_alpha.team_id.clone();
|
||||
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_common(FRIEND_USER_ID, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(1, notifications.len());
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn get_user_notifications_after_reading_indicates_notification_read() {
|
||||
with_test_environment_all(None, |test_env| async move {
|
||||
test_env.generate_friend_user_notification().await;
|
||||
let api = test_env.api;
|
||||
let notifications = api
|
||||
.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();
|
||||
|
||||
api.mark_notification_read(¬ification_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
|
||||
let notifications = api
|
||||
.get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(1, notifications.len());
|
||||
assert!(notifications[0].read);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn get_user_notifications_after_deleting_does_not_show_notification() {
|
||||
with_test_environment_all(None, |test_env| async move {
|
||||
test_env.generate_friend_user_notification().await;
|
||||
let api = test_env.api;
|
||||
let notifications = api
|
||||
.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();
|
||||
|
||||
api.delete_notification(¬ification_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
|
||||
let notifications = api
|
||||
.get_user_notifications_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(0, notifications.len());
|
||||
})
|
||||
.await;
|
||||
}
|
||||
309
apps/labrinth/tests/oauth.rs
Normal file
309
apps/labrinth/tests/oauth.rs
Normal file
@@ -0,0 +1,309 @@
|
||||
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},
|
||||
ApiV3,
|
||||
},
|
||||
database::FRIEND_USER_ID,
|
||||
database::{FRIEND_USER_PAT, USER_USER_ID, USER_USER_PAT},
|
||||
dummy_data::DummyOAuthClientAlpha,
|
||||
environment::{with_test_environment, TestEnvironment},
|
||||
};
|
||||
use labrinth::auth::oauth::TokenResponse;
|
||||
use reqwest::header::{CACHE_CONTROL, PRAGMA};
|
||||
|
||||
mod common;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn oauth_flow_happy_path() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let DummyOAuthClientAlpha {
|
||||
valid_redirect_uri: base_redirect_uri,
|
||||
client_id,
|
||||
client_secret,
|
||||
} = &env.dummy.oauth_client_alpha;
|
||||
|
||||
// Initiate authorization
|
||||
let redirect_uri = format!("{}?foo=bar", base_redirect_uri);
|
||||
let original_state = "1234";
|
||||
let resp = env
|
||||
.api
|
||||
.oauth_authorize(
|
||||
client_id,
|
||||
Some("USER_READ NOTIFICATION_READ"),
|
||||
Some(&redirect_uri),
|
||||
Some(original_state),
|
||||
FRIEND_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
let flow_id = get_authorize_accept_flow_id(resp).await;
|
||||
|
||||
// Accept the authorization request
|
||||
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);
|
||||
|
||||
let auth_code = query.get("code").unwrap();
|
||||
let state = query.get("state").unwrap();
|
||||
let foo_val = query.get("foo").unwrap();
|
||||
assert_eq!(state, original_state);
|
||||
assert_eq!(foo_val, "bar");
|
||||
|
||||
// Get the token
|
||||
let resp = env
|
||||
.api
|
||||
.oauth_token(
|
||||
auth_code.to_string(),
|
||||
Some(redirect_uri.clone()),
|
||||
client_id.to_string(),
|
||||
client_secret,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
assert_eq!(resp.headers().get(CACHE_CONTROL).unwrap(), "no-store");
|
||||
assert_eq!(resp.headers().get(PRAGMA).unwrap(), "no-cache");
|
||||
let token_resp: TokenResponse = test::read_body_json(resp).await;
|
||||
|
||||
// Validate the token works
|
||||
env.assert_read_notifications_status(
|
||||
FRIEND_USER_ID,
|
||||
Some(&token_resp.access_token),
|
||||
StatusCode::OK,
|
||||
)
|
||||
.await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn oauth_authorize_for_already_authorized_scopes_returns_auth_code() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let DummyOAuthClientAlpha { client_id, .. } = env.dummy.oauth_client_alpha;
|
||||
|
||||
let resp = env
|
||||
.api
|
||||
.oauth_authorize(
|
||||
&client_id,
|
||||
Some("USER_READ NOTIFICATION_READ"),
|
||||
None,
|
||||
Some("1234"),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
let flow_id = get_authorize_accept_flow_id(resp).await;
|
||||
env.api.oauth_accept(&flow_id, USER_USER_PAT).await;
|
||||
|
||||
let resp = env
|
||||
.api
|
||||
.oauth_authorize(
|
||||
&client_id,
|
||||
Some("USER_READ"),
|
||||
None,
|
||||
Some("5678"),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn get_oauth_token_with_already_used_auth_code_fails() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let DummyOAuthClientAlpha {
|
||||
client_id,
|
||||
client_secret,
|
||||
..
|
||||
} = env.dummy.oauth_client_alpha;
|
||||
|
||||
let resp = env
|
||||
.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.api.oauth_accept(&flow_id, USER_USER_PAT).await;
|
||||
let auth_code = get_auth_code_from_redirect_params(&resp).await;
|
||||
|
||||
let resp = env
|
||||
.api
|
||||
.oauth_token(auth_code.clone(), None, client_id.clone(), &client_secret)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
let resp = env
|
||||
.api
|
||||
.oauth_token(auth_code, None, client_id, &client_secret)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn authorize_with_broader_scopes_can_complete_flow() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let DummyOAuthClientAlpha {
|
||||
client_id,
|
||||
client_secret,
|
||||
..
|
||||
} = env.dummy.oauth_client_alpha.clone();
|
||||
|
||||
let first_access_token = env
|
||||
.api
|
||||
.complete_full_authorize_flow(
|
||||
&client_id,
|
||||
&client_secret,
|
||||
Some("PROJECT_READ"),
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
let second_access_token = env
|
||||
.api
|
||||
.complete_full_authorize_flow(
|
||||
&client_id,
|
||||
&client_secret,
|
||||
Some("PROJECT_READ NOTIFICATION_READ"),
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
|
||||
env.assert_read_notifications_status(
|
||||
USER_USER_ID,
|
||||
Some(&first_access_token),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
)
|
||||
.await;
|
||||
env.assert_read_user_projects_status(
|
||||
USER_USER_ID,
|
||||
Some(&first_access_token),
|
||||
StatusCode::OK,
|
||||
)
|
||||
.await;
|
||||
|
||||
env.assert_read_notifications_status(
|
||||
USER_USER_ID,
|
||||
Some(&second_access_token),
|
||||
StatusCode::OK,
|
||||
)
|
||||
.await;
|
||||
env.assert_read_user_projects_status(
|
||||
USER_USER_ID,
|
||||
Some(&second_access_token),
|
||||
StatusCode::OK,
|
||||
)
|
||||
.await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn oauth_authorize_with_broader_scopes_requires_user_accept() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let client_id = env.dummy.oauth_client_alpha.client_id;
|
||||
let resp = env
|
||||
.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.api.oauth_accept(&flow_id, USER_USER_PAT).await;
|
||||
|
||||
let resp = env
|
||||
.api
|
||||
.oauth_authorize(
|
||||
&client_id,
|
||||
Some("USER_READ NOTIFICATION_READ"),
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
get_authorize_accept_flow_id(resp).await; // ensure we can deser this without error to really confirm
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn reject_authorize_ends_authorize_flow() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let client_id = env.dummy.oauth_client_alpha.client_id;
|
||||
let resp = env
|
||||
.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.api.oauth_reject(&flow_id, USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
let resp = env.api.oauth_accept(&flow_id, USER_USER_PAT).await;
|
||||
assert_any_status_except!(&resp, StatusCode::OK);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn accept_authorize_after_already_accepting_fails() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let client_id = env.dummy.oauth_client_alpha.client_id;
|
||||
let resp = env
|
||||
.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.api.oauth_accept(&flow_id, USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
let resp = env.api.oauth_accept(&flow_id, USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn revoke_authorization_after_issuing_token_revokes_token() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let DummyOAuthClientAlpha {
|
||||
client_id,
|
||||
client_secret,
|
||||
..
|
||||
} = &env.dummy.oauth_client_alpha;
|
||||
let access_token = env
|
||||
.api
|
||||
.complete_full_authorize_flow(
|
||||
client_id,
|
||||
client_secret,
|
||||
Some("NOTIFICATION_READ"),
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
env.assert_read_notifications_status(USER_USER_ID, Some(&access_token), StatusCode::OK)
|
||||
.await;
|
||||
|
||||
let resp = env
|
||||
.api
|
||||
.revoke_oauth_authorization(client_id, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
env.assert_read_notifications_status(
|
||||
USER_USER_ID,
|
||||
Some(&access_token),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
)
|
||||
.await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
184
apps/labrinth/tests/oauth_clients.rs
Normal file
184
apps/labrinth/tests/oauth_clients.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
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, TestEnvironment},
|
||||
get_json_val_str,
|
||||
};
|
||||
use labrinth::{
|
||||
models::{
|
||||
oauth_clients::{OAuthClient, OAuthClientCreationResult},
|
||||
pats::Scopes,
|
||||
},
|
||||
routes::v3::oauth_clients::OAuthClientEdit,
|
||||
};
|
||||
|
||||
use common::database::USER_USER_ID_PARSED;
|
||||
|
||||
mod common;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn can_create_edit_get_oauth_client() {
|
||||
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
|
||||
.api
|
||||
.add_oauth_client(
|
||||
client_name.clone(),
|
||||
Scopes::all() - Scopes::restricted(),
|
||||
redirect_uris.clone(),
|
||||
FRIEND_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
let creation_result: OAuthClientCreationResult = test::read_body_json(resp).await;
|
||||
let client_id = get_json_val_str(creation_result.client.id);
|
||||
|
||||
let url = Some("https://modrinth.com".to_string());
|
||||
let description = Some("test description".to_string());
|
||||
let edited_redirect_uris = vec![
|
||||
redirect_uris[0].clone(),
|
||||
"https://modrinth.com/b".to_string(),
|
||||
];
|
||||
let edit = OAuthClientEdit {
|
||||
name: None,
|
||||
max_scopes: None,
|
||||
redirect_uris: Some(edited_redirect_uris.clone()),
|
||||
url: Some(url.clone()),
|
||||
description: Some(description.clone()),
|
||||
};
|
||||
let resp = env
|
||||
.api
|
||||
.edit_oauth_client(&client_id, edit, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
let clients = env
|
||||
.api
|
||||
.get_user_oauth_clients(FRIEND_USER_ID, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(1, clients.len());
|
||||
assert_eq!(url, clients[0].url);
|
||||
assert_eq!(description, clients[0].description);
|
||||
assert_eq!(client_name, clients[0].name);
|
||||
assert_eq!(2, clients[0].redirect_uris.len());
|
||||
assert_eq!(edited_redirect_uris[0], clients[0].redirect_uris[0].uri);
|
||||
assert_eq!(edited_redirect_uris[1], clients[0].redirect_uris[1].uri);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn create_oauth_client_with_restricted_scopes_fails() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let resp = env
|
||||
.api
|
||||
.add_oauth_client(
|
||||
"test_client".to_string(),
|
||||
Scopes::restricted(),
|
||||
vec!["https://modrinth.com".to_string()],
|
||||
FRIEND_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn get_oauth_client_for_client_creator_succeeds() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let DummyOAuthClientAlpha { client_id, .. } = env.dummy.oauth_client_alpha.clone();
|
||||
|
||||
let resp = env
|
||||
.api
|
||||
.get_oauth_client(client_id.clone(), USER_USER_PAT)
|
||||
.await;
|
||||
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
let client: OAuthClient = test::read_body_json(resp).await;
|
||||
assert_eq!(get_json_val_str(client.id), client_id);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn can_delete_oauth_client() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let client_id = env.dummy.oauth_client_alpha.client_id.clone();
|
||||
let resp = env.api.delete_oauth_client(&client_id, USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let clients = env
|
||||
.api
|
||||
.get_user_oauth_clients(USER_USER_ID, USER_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(0, clients.len());
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn delete_oauth_client_after_issuing_access_tokens_revokes_tokens() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let DummyOAuthClientAlpha {
|
||||
client_id,
|
||||
client_secret,
|
||||
..
|
||||
} = env.dummy.oauth_client_alpha.clone();
|
||||
let access_token = env
|
||||
.api
|
||||
.complete_full_authorize_flow(
|
||||
&client_id,
|
||||
&client_secret,
|
||||
Some("NOTIFICATION_READ"),
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
|
||||
env.api.delete_oauth_client(&client_id, USER_USER_PAT).await;
|
||||
|
||||
env.assert_read_notifications_status(
|
||||
USER_USER_ID,
|
||||
Some(&access_token),
|
||||
StatusCode::UNAUTHORIZED,
|
||||
)
|
||||
.await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn can_list_user_oauth_authorizations() {
|
||||
with_test_environment(None, |env: TestEnvironment<ApiV3>| async move {
|
||||
let DummyOAuthClientAlpha {
|
||||
client_id,
|
||||
client_secret,
|
||||
..
|
||||
} = env.dummy.oauth_client_alpha.clone();
|
||||
env.api
|
||||
.complete_full_authorize_flow(
|
||||
&client_id,
|
||||
&client_secret,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
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);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
1087
apps/labrinth/tests/organizations.rs
Normal file
1087
apps/labrinth/tests/organizations.rs
Normal file
File diff suppressed because it is too large
Load Diff
294
apps/labrinth/tests/pats.rs
Normal file
294
apps/labrinth/tests/pats.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::test;
|
||||
use chrono::{Duration, Utc};
|
||||
use common::{database::*, environment::with_test_environment_all};
|
||||
|
||||
use labrinth::models::pats::Scopes;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::common::api_common::AppendsOptionalPat;
|
||||
|
||||
mod common;
|
||||
|
||||
// Full pat test:
|
||||
// - create a PAT and ensure it can be used for the scope
|
||||
// - ensure access token is not returned for any PAT in GET
|
||||
// - ensure PAT can be patched to change scopes
|
||||
// - ensure PAT can be patched to change expiry
|
||||
// - ensure expired PATs cannot be used
|
||||
// - ensure PATs can be deleted
|
||||
#[actix_rt::test]
|
||||
pub async fn pat_full_test() {
|
||||
with_test_environment_all(None, |test_env| async move {
|
||||
// Create a PAT for a full test
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/_internal/pat")
|
||||
.append_pat(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_status!(&resp, StatusCode::OK);
|
||||
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();
|
||||
|
||||
// Get PAT again
|
||||
let req = test::TestRequest::get()
|
||||
.append_pat(USER_USER_PAT)
|
||||
.uri("/_internal/pat")
|
||||
.to_request();
|
||||
let resp = test_env.call(req).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
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 {
|
||||
// This uses a route directly instead of an api call because it doesn't relaly matter and we
|
||||
// want it to succeed no matter what.
|
||||
// This is an arbitrary request.
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/v3/collection")
|
||||
.append_header(("Authorization", token))
|
||||
.set_json(json!({
|
||||
"name": "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!("/_internal/pat/{}", id))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"scopes": 0,
|
||||
}))
|
||||
.to_request();
|
||||
let resp = test_env.call(req).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
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!("/_internal/pat/{}", id))
|
||||
.append_pat(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_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// 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!("/_internal/pat/{}", id))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"expires": Utc::now() + Duration::days(1), // no longer expired!
|
||||
}))
|
||||
.to_request();
|
||||
|
||||
println!("PAT ID FOR TEST: {}", id);
|
||||
let resp = test_env.call(req).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
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!("/_internal/pat/{}", id))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"expires": Utc::now() - Duration::days(1), // Past
|
||||
}))
|
||||
.to_request();
|
||||
let resp = test_env.call(req).await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// 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!("/_internal/pat/{}", id))
|
||||
.append_pat(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_pat(USER_USER_PAT)
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.to_request();
|
||||
let resp = test_env.call(req).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
// Test illegal PAT setting, both in POST and PATCH
|
||||
#[actix_rt::test]
|
||||
pub async fn bad_pats() {
|
||||
with_test_environment_all(None, |test_env| async move {
|
||||
// Creating a PAT with no name should fail
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/_internal/pat")
|
||||
.append_pat(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_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// 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("/_internal/pat")
|
||||
.append_pat(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_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Creating a PAT with an expiry in the past should fail
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/_internal/pat")
|
||||
.append_pat(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_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// 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("/_internal/pat")
|
||||
.append_pat(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("/_internal/pat")
|
||||
.append_pat(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_status!(&resp, StatusCode::OK);
|
||||
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("/_internal/pat")
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"name": name,
|
||||
}))
|
||||
.to_request();
|
||||
let resp = test_env.call(req).await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Patching to a bad expiry should fail
|
||||
let req = test::TestRequest::patch()
|
||||
.uri(&format!("/_internal/pat/{}", id))
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_json(json!({
|
||||
"expires": Utc::now() - Duration::days(1), // Past
|
||||
}))
|
||||
.to_request();
|
||||
let resp = test_env.call(req).await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// 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!("/_internal/pat/{}", id))
|
||||
.append_pat(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;
|
||||
}
|
||||
1283
apps/labrinth/tests/project.rs
Normal file
1283
apps/labrinth/tests/project.rs
Normal file
File diff suppressed because it is too large
Load Diff
1163
apps/labrinth/tests/scopes.rs
Normal file
1163
apps/labrinth/tests/scopes.rs
Normal file
File diff suppressed because it is too large
Load Diff
169
apps/labrinth/tests/search.rs
Normal file
169
apps/labrinth/tests/search.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use actix_http::StatusCode;
|
||||
use common::api_v3::ApiV3;
|
||||
use common::database::*;
|
||||
|
||||
use common::dummy_data::DUMMY_CATEGORIES;
|
||||
|
||||
use common::environment::with_test_environment;
|
||||
use common::environment::TestEnvironment;
|
||||
use common::search::setup_search_projects;
|
||||
use futures::stream::StreamExt;
|
||||
use labrinth::models::ids::base62_impl::parse_base62;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::common::api_common::Api;
|
||||
use crate::common::api_common::ApiProject;
|
||||
|
||||
mod common;
|
||||
|
||||
// TODO: Revisit this wit h the new modify_json in the version maker
|
||||
// That change here should be able to simplify it vastly
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn search_projects() {
|
||||
// Test setup and dummy data
|
||||
with_test_environment(Some(10), |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let id_conversion = setup_search_projects(&test_env).await;
|
||||
|
||||
let api = &test_env.api;
|
||||
let test_name = test_env.db.database_name.clone();
|
||||
|
||||
// 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, 9],
|
||||
),
|
||||
(json!([["categories:forge"]]), vec![7]),
|
||||
(
|
||||
json!([["categories:fabric", "categories:forge"]]),
|
||||
vec![0, 1, 2, 3, 4, 5, 6, 7, 9],
|
||||
),
|
||||
(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_only:true"]]), vec![0, 2, 3, 7, 9]),
|
||||
(json!([["server_only:true"]]), vec![0, 2, 3, 6, 7]),
|
||||
(json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7, 9]),
|
||||
(json!([["license:MIT"]]), vec![1, 2, 4, 9]),
|
||||
(json!([[r#"name:'Mysterious Project'"#]]), vec![2, 3]),
|
||||
(json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7, 9]), // Organization test '9' is included here as user is owner of org
|
||||
(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:fabric"]]),
|
||||
// vec![4],
|
||||
// ),
|
||||
(
|
||||
json!([["categories:fabric"], ["project_types:modpack"]]),
|
||||
vec![4],
|
||||
),
|
||||
];
|
||||
// TODO: versions, game versions
|
||||
// Untested:
|
||||
// - downloads (not varied)
|
||||
// - color (not varied)
|
||||
// - created_timestamp (not varied)
|
||||
// - modified_timestamp (not varied)
|
||||
// TODO: multiple different project types test
|
||||
|
||||
// Test searches
|
||||
let stream = futures::stream::iter(pairs);
|
||||
stream
|
||||
.for_each_concurrent(1, |(facets, mut expected_project_ids)| {
|
||||
let id_conversion = id_conversion.clone();
|
||||
let test_name = test_name.clone();
|
||||
async move {
|
||||
let projects = api
|
||||
.search_deserialized(
|
||||
Some(&format!("\"&{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();
|
||||
let num_hits = projects.total_hits;
|
||||
expected_project_ids.sort();
|
||||
found_project_ids.sort();
|
||||
println!("Facets: {:?}", facets);
|
||||
assert_eq!(found_project_ids, expected_project_ids);
|
||||
assert_eq!(num_hits, { expected_project_ids.len() });
|
||||
}
|
||||
})
|
||||
.await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn index_swaps() {
|
||||
with_test_environment(Some(10), |test_env: TestEnvironment<ApiV3>| async move {
|
||||
// Reindex
|
||||
let resp = test_env.api.reset_search_index().await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// Now we should get results
|
||||
let projects = test_env
|
||||
.api
|
||||
.search_deserialized(None, Some(json!([["categories:fabric"]])), USER_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(projects.total_hits, 1);
|
||||
assert!(projects.hits[0].slug.as_ref().unwrap().contains("alpha"));
|
||||
|
||||
// Delete the project
|
||||
let resp = test_env.api.remove_project("alpha", USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// We should not get any results, because the project has been deleted
|
||||
let projects = test_env
|
||||
.api
|
||||
.search_deserialized(None, Some(json!([["categories:fabric"]])), USER_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(projects.total_hits, 0);
|
||||
|
||||
// But when we reindex, it should be gone
|
||||
let resp = test_env.api.reset_search_index().await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let projects = test_env
|
||||
.api
|
||||
.search_deserialized(None, Some(json!([["categories:fabric"]])), USER_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(projects.total_hits, 0);
|
||||
|
||||
// Reindex again, should still be gone
|
||||
let resp = test_env.api.reset_search_index().await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let projects = test_env
|
||||
.api
|
||||
.search_deserialized(None, Some(json!([["categories:fabric"]])), USER_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(projects.total_hits, 0);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
64
apps/labrinth/tests/tags.rs
Normal file
64
apps/labrinth/tests/tags.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use common::{
|
||||
api_v3::ApiV3,
|
||||
environment::{with_test_environment, with_test_environment_all, TestEnvironment},
|
||||
};
|
||||
|
||||
use crate::common::api_common::ApiTags;
|
||||
|
||||
mod common;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn get_tags() {
|
||||
with_test_environment_all(None, |test_env| async move {
|
||||
let api = &test_env.api;
|
||||
let categories = api.get_categories_deserialized_common().await;
|
||||
|
||||
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()
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn get_tags_v3() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let api = &test_env.api;
|
||||
let loaders = api.get_loaders_deserialized().await;
|
||||
|
||||
let loader_metadata = loaders
|
||||
.into_iter()
|
||||
.map(|x| (x.name, x.metadata.get("platform").and_then(|x| x.as_bool())))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let loader_names = loader_metadata.keys().cloned().collect::<HashSet<String>>();
|
||||
assert_eq!(
|
||||
loader_names,
|
||||
["fabric", "forge", "mrpack", "bukkit", "waterfall"]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
);
|
||||
assert_eq!(loader_metadata["fabric"], None);
|
||||
assert_eq!(loader_metadata["bukkit"], Some(false));
|
||||
assert_eq!(loader_metadata["waterfall"], Some(true));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
621
apps/labrinth/tests/teams.rs
Normal file
621
apps/labrinth/tests/teams.rs
Normal file
@@ -0,0 +1,621 @@
|
||||
use crate::common::{api_common::ApiTeams, database::*};
|
||||
use actix_http::StatusCode;
|
||||
use common::{
|
||||
api_v3::ApiV3,
|
||||
environment::{with_test_environment, with_test_environment_all, TestEnvironment},
|
||||
};
|
||||
use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions};
|
||||
use rust_decimal::Decimal;
|
||||
use serde_json::json;
|
||||
|
||||
mod common;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_get_team() {
|
||||
// Test setup and dummy data
|
||||
// Perform get_team related tests for a project team
|
||||
//TODO: This needs to consider organizations now as well
|
||||
with_test_environment_all(None, |test_env| async move {
|
||||
let api = &test_env.api;
|
||||
let alpha_project_id = &test_env.dummy.project_alpha.project_id;
|
||||
let alpha_team_id = &test_env.dummy.project_alpha.team_id;
|
||||
|
||||
// A non-member of the team should get basic info but not be able to see private data
|
||||
let members = api
|
||||
.get_team_members_deserialized_common(alpha_team_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(members.len(), 1);
|
||||
assert_eq!(members[0].user.id.0, USER_USER_ID_PARSED as u64);
|
||||
assert!(members[0].permissions.is_none());
|
||||
|
||||
let members = api
|
||||
.get_project_members_deserialized_common(alpha_project_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(members.len(), 1);
|
||||
assert_eq!(members[0].user.id.0, USER_USER_ID_PARSED as u64);
|
||||
|
||||
// A non-accepted member of the team should:
|
||||
// - not be able to see private data about the team, but see all members including themselves
|
||||
// - should not appear in the team members list to enemy users
|
||||
let resp = api
|
||||
.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// Team check directly
|
||||
let members = api
|
||||
.get_team_members_deserialized_common(alpha_team_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team
|
||||
let user_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
let friend_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64);
|
||||
assert!(user_user.permissions.is_none()); // Should not see private data of the team
|
||||
assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64);
|
||||
assert!(friend_user.permissions.is_none());
|
||||
|
||||
// team check via association
|
||||
let members = api
|
||||
.get_project_members_deserialized_common(alpha_project_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team
|
||||
let user_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
let friend_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64);
|
||||
assert!(user_user.permissions.is_none()); // Should not see private data of the team
|
||||
assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64);
|
||||
assert!(friend_user.permissions.is_none());
|
||||
|
||||
// enemy team check directly
|
||||
let members = api
|
||||
.get_team_members_deserialized_common(alpha_team_id, ENEMY_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team
|
||||
|
||||
// enemy team check via association
|
||||
let members = api
|
||||
.get_project_members_deserialized_common(alpha_project_id, ENEMY_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team
|
||||
|
||||
// An accepted member of the team should appear in the team members list
|
||||
// and should be able to see private data about the team
|
||||
let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// Team check directly
|
||||
let members = api
|
||||
.get_team_members_deserialized_common(alpha_team_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team
|
||||
let user_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
let friend_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64);
|
||||
assert!(user_user.permissions.is_some()); // SHOULD see private data of the team
|
||||
assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64);
|
||||
assert!(friend_user.permissions.is_some());
|
||||
|
||||
// team check via association
|
||||
let members = api
|
||||
.get_project_members_deserialized_common(alpha_project_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team
|
||||
let user_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
let friend_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64);
|
||||
assert!(user_user.permissions.is_some()); // SHOULD see private data of the team
|
||||
assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64);
|
||||
assert!(friend_user.permissions.is_some());
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_get_team_organization() {
|
||||
// Test setup and dummy data
|
||||
// Perform get_team related tests for an organization team
|
||||
//TODO: This needs to consider users in organizations now and how they perceive as well
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let api = &test_env.api;
|
||||
let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id;
|
||||
let zeta_team_id = &test_env.dummy.organization_zeta.team_id;
|
||||
|
||||
// A non-member of the team should get basic info but not be able to see private data
|
||||
let members = api
|
||||
.get_team_members_deserialized_common(zeta_team_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(members.len(), 1);
|
||||
assert_eq!(members[0].user.id.0, USER_USER_ID_PARSED as u64);
|
||||
assert!(members[0].permissions.is_none());
|
||||
|
||||
let members = api
|
||||
.get_organization_members_deserialized_common(zeta_organization_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(members.len(), 1);
|
||||
assert_eq!(members[0].user.id.0, USER_USER_ID_PARSED as u64);
|
||||
|
||||
// A non-accepted member of the team should:
|
||||
// - not be able to see private data about the team, but see all members including themselves
|
||||
// - should not appear in the team members list to enemy users
|
||||
let resp = api
|
||||
.add_user_to_team(zeta_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// Team check directly
|
||||
let members = api
|
||||
.get_team_members_deserialized_common(zeta_team_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team
|
||||
let user_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
let friend_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64);
|
||||
assert!(user_user.permissions.is_none()); // Should not see private data of the team
|
||||
assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64);
|
||||
assert!(friend_user.permissions.is_none());
|
||||
|
||||
// team check via association
|
||||
let members = api
|
||||
.get_organization_members_deserialized_common(zeta_organization_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
|
||||
assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team
|
||||
let user_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
let friend_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64);
|
||||
assert!(user_user.permissions.is_none()); // Should not see private data of the team
|
||||
assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64);
|
||||
assert!(friend_user.permissions.is_none());
|
||||
|
||||
// enemy team check directly
|
||||
let members = api
|
||||
.get_team_members_deserialized_common(zeta_team_id, ENEMY_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team
|
||||
|
||||
// enemy team check via association
|
||||
let members = api
|
||||
.get_organization_members_deserialized_common(zeta_organization_id, ENEMY_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team
|
||||
|
||||
// An accepted member of the team should appear in the team members list
|
||||
// and should be able to see private data about the team
|
||||
let resp = api.join_team(zeta_team_id, FRIEND_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// Team check directly
|
||||
let members = api
|
||||
.get_team_members_deserialized_common(zeta_team_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team
|
||||
let user_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
let friend_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64);
|
||||
assert!(user_user.permissions.is_some()); // SHOULD see private data of the team
|
||||
assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64);
|
||||
assert!(friend_user.permissions.is_some());
|
||||
|
||||
// team check via association
|
||||
let members = api
|
||||
.get_organization_members_deserialized_common(zeta_organization_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team
|
||||
let user_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
let friend_user = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(user_user.user.id.0, USER_USER_ID_PARSED as u64);
|
||||
assert!(user_user.permissions.is_some()); // SHOULD see private data of the team
|
||||
assert_eq!(friend_user.user.id.0, FRIEND_USER_ID_PARSED as u64);
|
||||
assert!(friend_user.permissions.is_some());
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_get_team_project_orgs() {
|
||||
// Test setup and dummy data
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let alpha_project_id = &test_env.dummy.project_alpha.project_id;
|
||||
let alpha_team_id = &test_env.dummy.project_alpha.team_id;
|
||||
let zeta_organization_id = &test_env.dummy.organization_zeta.organization_id;
|
||||
let zeta_team_id = &test_env.dummy.organization_zeta.team_id;
|
||||
|
||||
// Attach alpha to zeta
|
||||
let resp = test_env
|
||||
.api
|
||||
.organization_add_project(zeta_organization_id, alpha_project_id, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
// Invite and add friend to zeta
|
||||
let resp = test_env
|
||||
.api
|
||||
.add_user_to_team(zeta_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let resp = test_env.api.join_team(zeta_team_id, FRIEND_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// The team members route from teams (on a project's team):
|
||||
// - the members of the project team specifically
|
||||
// - not the ones from the organization
|
||||
// - Remember: the owner of an org will not be included in the org's team members list
|
||||
let members = test_env
|
||||
.api
|
||||
.get_team_members_deserialized_common(alpha_team_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(members.len(), 0);
|
||||
|
||||
// The team members route from project should show the same!
|
||||
let members = test_env
|
||||
.api
|
||||
.get_project_members_deserialized_common(alpha_project_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(members.len(), 0);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
// edit team member (Varying permissions, varying roles)
|
||||
#[actix_rt::test]
|
||||
async fn test_patch_project_team_member() {
|
||||
// Test setup and dummy data
|
||||
with_test_environment_all(None, |test_env| async move {
|
||||
let api = &test_env.api;
|
||||
|
||||
let alpha_team_id = &test_env.dummy.project_alpha.team_id;
|
||||
|
||||
// Edit team as admin/mod but not a part of the team should be StatusCode::OK
|
||||
let resp = api.edit_team_member(alpha_team_id, USER_USER_ID, json!({}), ADMIN_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// As a non-owner with full permissions, attempt to edit the owner's permissions
|
||||
let resp = api.edit_team_member(alpha_team_id, USER_USER_ID, json!({
|
||||
"permissions": 0
|
||||
}), ADMIN_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// Should not be able to edit organization permissions of a project team
|
||||
let resp = api.edit_team_member(alpha_team_id, USER_USER_ID, json!({
|
||||
"organization_permissions": 0
|
||||
}), USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// Should not be able to add permissions to a user that the adding-user does not have
|
||||
// (true for both project and org)
|
||||
|
||||
// first, invite friend
|
||||
let resp = api.add_user_to_team(alpha_team_id, FRIEND_USER_ID,
|
||||
Some(ProjectPermissions::EDIT_MEMBER | ProjectPermissions::EDIT_BODY),
|
||||
None, USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// accept
|
||||
let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// try to add permissions
|
||||
let resp = api.edit_team_member(alpha_team_id, FRIEND_USER_ID, json!({
|
||||
"permissions": (ProjectPermissions::EDIT_MEMBER | ProjectPermissions::EDIT_DETAILS).bits()
|
||||
}), FRIEND_USER_PAT).await; // should this be friend_user_pat
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// Cannot set payouts outside of 0 and 5000
|
||||
for payout in [-1, 5001] {
|
||||
let resp = api.edit_team_member(alpha_team_id, FRIEND_USER_ID, json!({
|
||||
"payouts_split": payout
|
||||
}), USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Successful patch
|
||||
let resp = api.edit_team_member(alpha_team_id, FRIEND_USER_ID, json!({
|
||||
"payouts_split": 51,
|
||||
"permissions": ProjectPermissions::EDIT_MEMBER.bits(), // reduces permissions
|
||||
"role": "membe2r",
|
||||
"ordering": 5
|
||||
}), FRIEND_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// Check results
|
||||
let members = api.get_team_members_deserialized_common(alpha_team_id, FRIEND_USER_PAT).await;
|
||||
let member = members.iter().find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64).unwrap();
|
||||
assert_eq!(member.payouts_split, Decimal::from_f64_retain(51.0));
|
||||
assert_eq!(member.permissions.unwrap(), ProjectPermissions::EDIT_MEMBER);
|
||||
assert_eq!(member.role, "membe2r");
|
||||
assert_eq!(member.ordering, 5);
|
||||
}).await;
|
||||
}
|
||||
|
||||
// edit team member (Varying permissions, varying roles)
|
||||
#[actix_rt::test]
|
||||
async fn test_patch_organization_team_member() {
|
||||
// Test setup and dummy data
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let zeta_team_id = &test_env.dummy.organization_zeta.team_id;
|
||||
|
||||
// Edit team as admin/mod but not a part of the team should be StatusCode::OK
|
||||
let resp = test_env
|
||||
.api
|
||||
.edit_team_member(zeta_team_id, USER_USER_ID, json!({}), ADMIN_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// As a non-owner with full permissions, attempt to edit the owner's permissions
|
||||
let resp = test_env
|
||||
.api
|
||||
.edit_team_member(zeta_team_id, USER_USER_ID, json!({ "permissions": 0 }), ADMIN_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// Should not be able to add permissions to a user that the adding-user does not have
|
||||
// (true for both project and org)
|
||||
|
||||
// first, invite friend
|
||||
let resp = test_env
|
||||
.api
|
||||
.add_user_to_team(zeta_team_id, FRIEND_USER_ID, None, Some(OrganizationPermissions::EDIT_MEMBER | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS), USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// accept
|
||||
let resp = test_env.api.join_team(zeta_team_id, FRIEND_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// try to add permissions- fails, as we do not have EDIT_DETAILS
|
||||
let resp = test_env
|
||||
.api
|
||||
.edit_team_member(zeta_team_id, FRIEND_USER_ID, json!({ "organization_permissions": (OrganizationPermissions::EDIT_MEMBER | OrganizationPermissions::EDIT_DETAILS).bits() }), FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// Cannot set payouts outside of 0 and 5000
|
||||
for payout in [-1, 5001] {
|
||||
let resp = test_env
|
||||
.api
|
||||
.edit_team_member(zeta_team_id, FRIEND_USER_ID, json!({ "payouts_split": payout }), USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
// Successful patch
|
||||
let resp = test_env
|
||||
.api
|
||||
.edit_team_member(
|
||||
zeta_team_id,
|
||||
FRIEND_USER_ID,
|
||||
json!({
|
||||
"payouts_split": 51,
|
||||
"organization_permissions": OrganizationPermissions::EDIT_MEMBER.bits(), // reduces permissions
|
||||
"permissions": (ProjectPermissions::EDIT_MEMBER).bits(),
|
||||
"role": "very-cool-member",
|
||||
"ordering": 5
|
||||
}),
|
||||
FRIEND_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// Check results
|
||||
let members = test_env
|
||||
.api
|
||||
.get_team_members_deserialized(zeta_team_id, FRIEND_USER_PAT)
|
||||
.await;
|
||||
let member = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(member.payouts_split.unwrap(), Decimal::from_f64_retain(51.0_f64).unwrap());
|
||||
assert_eq!(
|
||||
member.organization_permissions,
|
||||
Some(OrganizationPermissions::EDIT_MEMBER)
|
||||
);
|
||||
assert_eq!(
|
||||
member.permissions,
|
||||
Some(ProjectPermissions::EDIT_MEMBER)
|
||||
);
|
||||
assert_eq!(member.role, "very-cool-member");
|
||||
assert_eq!(member.ordering, 5);
|
||||
|
||||
}).await;
|
||||
}
|
||||
|
||||
// trasnfer ownership (requires being owner, etc)
|
||||
#[actix_rt::test]
|
||||
async fn transfer_ownership_v3() {
|
||||
// Test setup and dummy data
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
|
||||
let api = &test_env.api;
|
||||
|
||||
let alpha_team_id = &test_env.dummy.project_alpha.team_id;
|
||||
|
||||
// Cannot set friend as owner (not a member)
|
||||
let resp = api
|
||||
.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
let resp = api
|
||||
.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::UNAUTHORIZED);
|
||||
|
||||
// first, invite friend
|
||||
let resp = api
|
||||
.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// still cannot set friend as owner (not accepted)
|
||||
let resp = api
|
||||
.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// accept
|
||||
let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// Cannot set ourselves as owner if we are not owner
|
||||
let resp = api
|
||||
.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::UNAUTHORIZED);
|
||||
|
||||
// Can set friend as owner
|
||||
let resp = api
|
||||
.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// Check
|
||||
let members = api
|
||||
.get_team_members_deserialized(alpha_team_id, USER_USER_PAT)
|
||||
.await;
|
||||
let friend_member = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(friend_member.role, "Member"); // her role does not actually change, but is_owner is set to true
|
||||
assert!(friend_member.is_owner);
|
||||
assert_eq!(
|
||||
friend_member.permissions.unwrap(),
|
||||
ProjectPermissions::all()
|
||||
);
|
||||
|
||||
let user_member = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(user_member.role, "Member"); // We are the 'owner', but we are not actually the owner!
|
||||
assert!(!user_member.is_owner);
|
||||
assert_eq!(user_member.permissions.unwrap(), ProjectPermissions::all());
|
||||
|
||||
// Confirm that user, a user who still has full permissions, cannot then remove the owner
|
||||
let resp = api
|
||||
.remove_from_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::UNAUTHORIZED);
|
||||
|
||||
// V3 only- confirm the owner can change their role without losing ownership
|
||||
let resp = api
|
||||
.edit_team_member(
|
||||
alpha_team_id,
|
||||
FRIEND_USER_ID,
|
||||
json!({
|
||||
"role": "Member"
|
||||
}),
|
||||
FRIEND_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let members = api
|
||||
.get_team_members_deserialized(alpha_team_id, USER_USER_PAT)
|
||||
.await;
|
||||
let friend_member = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(friend_member.role, "Member");
|
||||
assert!(friend_member.is_owner);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
// This test is currently not working.
|
||||
// #[actix_rt::test]
|
||||
// pub async fn no_acceptance_permissions() {
|
||||
// // Adding a user to a project team in an organization, when that user is in the organization but not the team,
|
||||
// // should have those permissions apply regardless of whether the user has accepted the invite or not.
|
||||
|
||||
// // This is because project-team permission overrriding must be possible, and this overriding can decrease the number of permissions a user has.
|
||||
|
||||
// let test_env = TestEnvironment::build(None).await;
|
||||
// let api = &test_env.api;
|
||||
|
||||
// let alpha_team_id = &test_env.dummy.project_alpha.team_id;
|
||||
// let alpha_project_id = &test_env.dummy.project_alpha.project_id;
|
||||
// let zeta_organization_id = &test_env.dummy.zeta_organization_id;
|
||||
// let zeta_team_id = &test_env.dummy.zeta_team_id;
|
||||
|
||||
// // Link alpha team to zeta org
|
||||
// let resp = api.organization_add_project(zeta_organization_id, alpha_project_id, USER_USER_PAT).await;
|
||||
// assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
// // Invite friend to zeta team with all project default permissions
|
||||
// let resp = api.add_user_to_team(&zeta_team_id, FRIEND_USER_ID, Some(ProjectPermissions::all()), Some(OrganizationPermissions::all()), USER_USER_PAT).await;
|
||||
// assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// // Accept invite to zeta team
|
||||
// let resp = api.join_team(&zeta_team_id, FRIEND_USER_PAT).await;
|
||||
// assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// // Attempt, as friend, to edit details of alpha project (should succeed, org invite accepted)
|
||||
// let resp = api.edit_project(alpha_project_id, json!({
|
||||
// "title": "new name"
|
||||
// }), FRIEND_USER_PAT).await;
|
||||
// assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// // Invite friend to alpha team with *no* project permissions
|
||||
// let resp = api.add_user_to_team(&alpha_team_id, FRIEND_USER_ID, Some(ProjectPermissions::empty()), None, USER_USER_PAT).await;
|
||||
// assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// // Do not accept invite to alpha team
|
||||
|
||||
// // Attempt, as friend, to edit details of alpha project (should fail now, even though user has not accepted invite)
|
||||
// let resp = api.edit_project(alpha_project_id, json!({
|
||||
// "title": "new name"
|
||||
// }), FRIEND_USER_PAT).await;
|
||||
// assert_status!(&resp, StatusCode::UNAUTHORIZED);
|
||||
|
||||
// test_env.cleanup().await;
|
||||
// }
|
||||
105
apps/labrinth/tests/user.rs
Normal file
105
apps/labrinth/tests/user.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
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_all,
|
||||
};
|
||||
|
||||
mod common;
|
||||
|
||||
// user GET (permissions, different users)
|
||||
// users GET
|
||||
// user auth
|
||||
// user projects get
|
||||
// user collections get
|
||||
// patch user
|
||||
// patch user icon
|
||||
// user follows
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn get_user_projects_after_creating_project_returns_new_project() {
|
||||
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("slug", Some(TestFile::BasicMod), None, USER_USER_PAT)
|
||||
.await;
|
||||
|
||||
let resp_projects = api
|
||||
.get_user_projects_deserialized_common(USER_USER_ID, USER_USER_PAT)
|
||||
.await;
|
||||
assert!(resp_projects.iter().any(|p| p.id == project.id));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn get_user_projects_after_deleting_project_shows_removal() {
|
||||
with_test_environment_all(None, |test_env| async move {
|
||||
let api = test_env.api;
|
||||
let (project, _) = api
|
||||
.add_public_project("iota", Some(TestFile::BasicMod), None, USER_USER_PAT)
|
||||
.await;
|
||||
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_common(USER_USER_ID, USER_USER_PAT)
|
||||
.await;
|
||||
assert!(!resp_projects.iter().any(|p| p.id == project.id));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn get_user_projects_after_joining_team_shows_team_projects() {
|
||||
with_test_environment_all(None, |test_env| async move {
|
||||
let alpha_team_id = &test_env.dummy.project_alpha.team_id;
|
||||
let alpha_project_id = &test_env.dummy.project_alpha.project_id;
|
||||
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)
|
||||
.await;
|
||||
api.join_team(alpha_team_id, FRIEND_USER_PAT).await;
|
||||
|
||||
let projects = api
|
||||
.get_user_projects_deserialized_common(FRIEND_USER_ID, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert!(projects
|
||||
.iter()
|
||||
.any(|p| p.id.to_string() == *alpha_project_id));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn get_user_projects_after_leaving_team_shows_no_team_projects() {
|
||||
with_test_environment_all(None, |test_env| async move {
|
||||
let alpha_team_id = &test_env.dummy.project_alpha.team_id;
|
||||
let alpha_project_id = &test_env.dummy.project_alpha.project_id;
|
||||
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_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_common(FRIEND_USER_ID, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert!(!projects
|
||||
.iter()
|
||||
.any(|p| p.id.to_string() == *alpha_project_id));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
25
apps/labrinth/tests/v2/error.rs
Normal file
25
apps/labrinth/tests/v2/error.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::assert_status;
|
||||
use crate::common::api_common::ApiProject;
|
||||
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::test;
|
||||
use bytes::Bytes;
|
||||
|
||||
use crate::common::database::USER_USER_PAT;
|
||||
use crate::common::{
|
||||
api_v2::ApiV2,
|
||||
environment::{with_test_environment, TestEnvironment},
|
||||
};
|
||||
#[actix_rt::test]
|
||||
pub async fn error_404_empty() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
|
||||
// V2 errors should have 404 as blank body, for missing resources
|
||||
let api = &test_env.api;
|
||||
let resp = api.get_project("does-not-exist", USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NOT_FOUND);
|
||||
let body = test::read_body(resp).await;
|
||||
let empty_bytes = Bytes::from_static(b"");
|
||||
assert_eq!(body, empty_bytes);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
25
apps/labrinth/tests/v2/notifications.rs
Normal file
25
apps/labrinth/tests/v2/notifications.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::common::{
|
||||
api_common::ApiTeams,
|
||||
api_v2::ApiV2,
|
||||
database::{FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_PAT},
|
||||
environment::{with_test_environment, TestEnvironment},
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn get_user_notifications_after_team_invitation_returns_notification() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
|
||||
let alpha_team_id = test_env.dummy.project_alpha.team_id.clone();
|
||||
let api = test_env.api;
|
||||
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)
|
||||
.await;
|
||||
assert_eq!(1, notifications.len());
|
||||
|
||||
// Check to make sure type_ is correct
|
||||
assert_eq!(notifications[0].type_.as_ref().unwrap(), "team_invite");
|
||||
})
|
||||
.await;
|
||||
}
|
||||
631
apps/labrinth/tests/v2/project.rs
Normal file
631
apps/labrinth/tests/v2/project.rs
Normal file
@@ -0,0 +1,631 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::{
|
||||
api_common::{ApiProject, ApiVersion, AppendsOptionalPat},
|
||||
api_v2::{request_data::get_public_project_creation_data_json, ApiV2},
|
||||
database::{
|
||||
generate_random_name, ADMIN_USER_PAT, FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_PAT,
|
||||
},
|
||||
dummy_data::TestFile,
|
||||
environment::{with_test_environment, TestEnvironment},
|
||||
permissions::{PermissionsTest, PermissionsTestContext},
|
||||
},
|
||||
};
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::test;
|
||||
use futures::StreamExt;
|
||||
use itertools::Itertools;
|
||||
use labrinth::{
|
||||
database::models::project_item::PROJECTS_SLUGS_NAMESPACE,
|
||||
models::{ids::base62_impl::parse_base62, projects::ProjectId, teams::ProjectPermissions},
|
||||
util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData},
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_project_type_sanity() {
|
||||
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'
|
||||
for (mod_or_modpack, slug, file) in [
|
||||
("mod", "test-mod", TestFile::build_random_jar()),
|
||||
("modpack", "test-modpack", TestFile::build_random_mrpack()),
|
||||
] {
|
||||
// Create a modpack or mod
|
||||
// both are 'fabric' (but modpack is actually 'mrpack' behind the scenes, through v3,with fabric as a 'mrpack_loader')
|
||||
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();
|
||||
|
||||
// Check that the loader displays correctly as fabric from the version creation
|
||||
assert_eq!(test_project.loaders, vec!["fabric"]);
|
||||
assert_eq!(test_version[0].loaders, vec!["fabric"]);
|
||||
|
||||
// Check that the project type is correct when getting the project
|
||||
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);
|
||||
|
||||
// Check that the project type is correct when getting the version
|
||||
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"]
|
||||
);
|
||||
|
||||
// Edit the version loader to change it to 'forge'
|
||||
let resp = api
|
||||
.edit_version(
|
||||
&test_version[0].id.to_string(),
|
||||
json!({
|
||||
"loaders": ["forge"],
|
||||
}),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// Check that the project type is still correct when getting the project
|
||||
let project = api
|
||||
.get_project_deserialized(test_project_slug, USER_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(project.project_type, mod_or_modpack);
|
||||
assert_eq!(project.loaders, vec!["forge"]);
|
||||
|
||||
// Check that the project type is still correct when getting the version
|
||||
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!["forge"]
|
||||
);
|
||||
}
|
||||
|
||||
// As we get more complicated strucures with as v3 continues to expand, 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
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
|
||||
let api = &test_env.api;
|
||||
|
||||
// Generate test project data.
|
||||
let mut json_data =
|
||||
get_public_project_creation_data_json("demo", Some(&TestFile::BasicMod));
|
||||
|
||||
// Basic json
|
||||
let json_segment = MultipartSegment {
|
||||
name: "data".to_string(),
|
||||
filename: None,
|
||||
content_type: Some("application/json".to_string()),
|
||||
data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()),
|
||||
};
|
||||
|
||||
// Basic json, with a different file
|
||||
json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar");
|
||||
let json_diff_file_segment = MultipartSegment {
|
||||
data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()),
|
||||
..json_segment.clone()
|
||||
};
|
||||
|
||||
// Basic json, with a different file, and a different slug
|
||||
json_data["slug"] = json!("new_demo");
|
||||
json_data["initial_versions"][0]["file_parts"][0] = json!("basic-mod-different.jar");
|
||||
let json_diff_slug_file_segment = MultipartSegment {
|
||||
data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()),
|
||||
..json_segment.clone()
|
||||
};
|
||||
|
||||
let basic_mod_file = TestFile::BasicMod;
|
||||
let basic_mod_different_file = TestFile::BasicModDifferent;
|
||||
|
||||
// Basic file
|
||||
let file_segment = MultipartSegment {
|
||||
// 'Basic'
|
||||
name: basic_mod_file.filename(),
|
||||
filename: Some(basic_mod_file.filename()),
|
||||
content_type: basic_mod_file.content_type(),
|
||||
data: MultipartSegmentData::Binary(basic_mod_file.bytes()),
|
||||
};
|
||||
|
||||
// Differently named file, with the SAME content (for hash testing)
|
||||
let file_diff_name_segment = MultipartSegment {
|
||||
// 'Different'
|
||||
name: basic_mod_different_file.filename(),
|
||||
filename: Some(basic_mod_different_file.filename()),
|
||||
content_type: basic_mod_different_file.content_type(),
|
||||
// 'Basic'
|
||||
data: MultipartSegmentData::Binary(basic_mod_file.bytes()),
|
||||
};
|
||||
|
||||
// Differently named file, with different content
|
||||
let file_diff_name_content_segment = MultipartSegment {
|
||||
// 'Different'
|
||||
name: basic_mod_different_file.filename(),
|
||||
filename: Some(basic_mod_different_file.filename()),
|
||||
content_type: basic_mod_different_file.content_type(),
|
||||
data: MultipartSegmentData::Binary(basic_mod_different_file.bytes()),
|
||||
};
|
||||
|
||||
// Add a project- simple, should work.
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/v2/project")
|
||||
.append_pat(USER_USER_PAT)
|
||||
.set_multipart(vec![json_segment.clone(), file_segment.clone()])
|
||||
.to_request();
|
||||
let resp: actix_web::dev::ServiceResponse = test_env.call(req).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
// 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(basic_mod_file.bytes())
|
||||
.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_pat(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_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// Reusing with the same slug and a different file should fail
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/v2/project")
|
||||
.append_pat(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_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// Different slug, different file should succeed
|
||||
let req = test::TestRequest::post()
|
||||
.uri("/v2/project")
|
||||
.append_pat(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_status!(&resp, StatusCode::OK);
|
||||
|
||||
// Get
|
||||
let project = api.get_project_deserialized("demo", USER_USER_PAT).await;
|
||||
let id = project.id.to_string();
|
||||
|
||||
// Remove the project
|
||||
let resp = test_env.api.remove_project("demo", USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// 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_status!(&resp, StatusCode::NOT_FOUND);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn permissions_upload_version() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
|
||||
let alpha_project_id = &test_env.dummy.project_alpha.project_id;
|
||||
let alpha_version_id = &test_env.dummy.project_alpha.version_id;
|
||||
let alpha_team_id = &test_env.dummy.project_alpha.team_id;
|
||||
let alpha_file_hash = &test_env.dummy.project_alpha.file_hash;
|
||||
|
||||
let api = &test_env.api;
|
||||
let basic_mod_different_file = TestFile::BasicModDifferent;
|
||||
let upload_version = ProjectPermissions::UPLOAD_VERSION;
|
||||
|
||||
let req_gen = |ctx: PermissionsTestContext| async move {
|
||||
let project_id = ctx.project_id.unwrap();
|
||||
let project_id = ProjectId(parse_base62(&project_id).unwrap());
|
||||
api.add_public_version(
|
||||
project_id,
|
||||
"1.0.0",
|
||||
TestFile::BasicMod,
|
||||
None,
|
||||
None,
|
||||
ctx.test_pat.as_deref(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
PermissionsTest::new(&test_env)
|
||||
.simple_project_permissions_test(upload_version, req_gen)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Upload file to existing version
|
||||
// Uses alpha project, as it has an existing version
|
||||
let file_ref = Arc::new(basic_mod_different_file);
|
||||
let req_gen = |ctx: PermissionsTestContext| {
|
||||
let file_ref = file_ref.clone();
|
||||
async move {
|
||||
api.upload_file_to_version(alpha_version_id, &file_ref, ctx.test_pat.as_deref())
|
||||
.await
|
||||
}
|
||||
};
|
||||
PermissionsTest::new(&test_env)
|
||||
.with_existing_project(alpha_project_id, alpha_team_id)
|
||||
.with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true)
|
||||
.simple_project_permissions_test(upload_version, req_gen)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Patch version
|
||||
// Uses alpha project, as it has an existing version
|
||||
let req_gen = |ctx: PermissionsTestContext| async move {
|
||||
api.edit_version(
|
||||
alpha_version_id,
|
||||
json!({
|
||||
"name": "Basic Mod",
|
||||
}),
|
||||
ctx.test_pat.as_deref(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
PermissionsTest::new(&test_env)
|
||||
.with_existing_project(alpha_project_id, alpha_team_id)
|
||||
.with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true)
|
||||
.simple_project_permissions_test(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 = |ctx: PermissionsTestContext| async move {
|
||||
api.remove_version_file(alpha_file_hash, ctx.test_pat.as_deref())
|
||||
.await
|
||||
};
|
||||
|
||||
PermissionsTest::new(&test_env)
|
||||
.with_existing_project(alpha_project_id, alpha_team_id)
|
||||
.with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true)
|
||||
.simple_project_permissions_test(delete_version, req_gen)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Delete version
|
||||
// Uses alpha project, as it has an existing version
|
||||
let req_gen = |ctx: PermissionsTestContext| async move {
|
||||
api.remove_version(alpha_version_id, ctx.test_pat.as_deref())
|
||||
.await
|
||||
};
|
||||
PermissionsTest::new(&test_env)
|
||||
.with_existing_project(alpha_project_id, alpha_team_id)
|
||||
.with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true)
|
||||
.simple_project_permissions_test(delete_version, req_gen)
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn test_patch_v2() {
|
||||
// Hits V3-specific patchable fields
|
||||
// Other fields are tested in test_patch_project (the v2 version of that test)
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
|
||||
let api = &test_env.api;
|
||||
|
||||
let alpha_project_slug = &test_env.dummy.project_alpha.project_slug;
|
||||
|
||||
// Sucessful request to patch many fields.
|
||||
let resp = api
|
||||
.edit_project(
|
||||
alpha_project_slug,
|
||||
json!({
|
||||
"client_side": "unsupported",
|
||||
"server_side": "required",
|
||||
}),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let project = api
|
||||
.get_project_deserialized(alpha_project_slug, USER_USER_PAT)
|
||||
.await;
|
||||
|
||||
// Note: the original V2 value of this was "optional",
|
||||
// but Required/Optional is no longer a carried combination in v3, as the changes made were lossy.
|
||||
// Now, the test Required/Unsupported combination is tested instead.
|
||||
// Setting Required/Optional in v2 will not work, this is known and accepteed.
|
||||
assert_eq!(project.client_side.as_str(), "unsupported");
|
||||
assert_eq!(project.server_side.as_str(), "required");
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn permissions_patch_project_v2() {
|
||||
with_test_environment(Some(8), |test_env: TestEnvironment<ApiV2>| async move {
|
||||
let api = &test_env.api;
|
||||
|
||||
// For each permission covered by EDIT_DETAILS, ensure the permission is required
|
||||
let edit_details = ProjectPermissions::EDIT_DETAILS;
|
||||
let test_pairs = [
|
||||
("description", json!("description")),
|
||||
("issues_url", json!("https://issues.com")),
|
||||
("source_url", json!("https://source.com")),
|
||||
("wiki_url", json!("https://wiki.com")),
|
||||
(
|
||||
"donation_urls",
|
||||
json!([{
|
||||
"id": "paypal",
|
||||
"platform": "Paypal",
|
||||
"url": "https://paypal.com"
|
||||
}]),
|
||||
),
|
||||
("discord_url", json!("https://discord.com")),
|
||||
];
|
||||
|
||||
futures::stream::iter(test_pairs)
|
||||
.map(|(key, value)| {
|
||||
let test_env = test_env.clone();
|
||||
async move {
|
||||
let req_gen = |ctx: PermissionsTestContext| async {
|
||||
api.edit_project(
|
||||
&ctx.project_id.unwrap(),
|
||||
json!({
|
||||
key: if key == "slug" {
|
||||
json!(generate_random_name("randomslug"))
|
||||
} else {
|
||||
value.clone()
|
||||
},
|
||||
}),
|
||||
ctx.test_pat.as_deref(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
PermissionsTest::new(&test_env)
|
||||
.simple_project_permissions_test(edit_details, req_gen)
|
||||
.await
|
||||
.into_iter();
|
||||
}
|
||||
})
|
||||
.buffer_unordered(4)
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
// Edit body
|
||||
// Cannot bulk edit body
|
||||
let edit_body = ProjectPermissions::EDIT_BODY;
|
||||
let req_gen = |ctx: PermissionsTestContext| async move {
|
||||
api.edit_project(
|
||||
&ctx.project_id.unwrap(),
|
||||
json!({
|
||||
"body": "new body!", // new body
|
||||
}),
|
||||
ctx.test_pat.as_deref(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
PermissionsTest::new(&test_env)
|
||||
.simple_project_permissions_test(edit_body, req_gen)
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn test_bulk_edit_links() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
|
||||
let api = &test_env.api;
|
||||
let alpha_project_id: &str = &test_env.dummy.project_alpha.project_id;
|
||||
let beta_project_id: &str = &test_env.dummy.project_beta.project_id;
|
||||
|
||||
let resp = api
|
||||
.edit_project_bulk(
|
||||
&[alpha_project_id, beta_project_id],
|
||||
json!({
|
||||
"issues_url": "https://github.com",
|
||||
"donation_urls": [
|
||||
{
|
||||
"id": "patreon",
|
||||
"platform": "Patreon",
|
||||
"url": "https://www.patreon.com/my_user"
|
||||
}
|
||||
],
|
||||
}),
|
||||
ADMIN_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let alpha_body = api
|
||||
.get_project_deserialized(alpha_project_id, ADMIN_USER_PAT)
|
||||
.await;
|
||||
let donation_urls = alpha_body.donation_urls.unwrap();
|
||||
assert_eq!(donation_urls.len(), 1);
|
||||
assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user");
|
||||
assert_eq!(
|
||||
alpha_body.issues_url,
|
||||
Some("https://github.com".to_string())
|
||||
);
|
||||
assert_eq!(alpha_body.discord_url, None);
|
||||
|
||||
let beta_body = api
|
||||
.get_project_deserialized(beta_project_id, ADMIN_USER_PAT)
|
||||
.await;
|
||||
let donation_urls = beta_body.donation_urls.unwrap();
|
||||
assert_eq!(donation_urls.len(), 1);
|
||||
assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user");
|
||||
assert_eq!(beta_body.issues_url, Some("https://github.com".to_string()));
|
||||
assert_eq!(beta_body.discord_url, None);
|
||||
|
||||
let resp = api
|
||||
.edit_project_bulk(
|
||||
&[alpha_project_id, beta_project_id],
|
||||
json!({
|
||||
"discord_url": "https://discord.gg",
|
||||
"issues_url": null,
|
||||
"add_donation_urls": [
|
||||
{
|
||||
"id": "bmac",
|
||||
"platform": "Buy Me a Coffee",
|
||||
"url": "https://www.buymeacoffee.com/my_user"
|
||||
}
|
||||
],
|
||||
}),
|
||||
ADMIN_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let alpha_body = api
|
||||
.get_project_deserialized(alpha_project_id, ADMIN_USER_PAT)
|
||||
.await;
|
||||
let donation_urls = alpha_body
|
||||
.donation_urls
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.sorted_by_key(|x| x.id.clone())
|
||||
.collect_vec();
|
||||
assert_eq!(donation_urls.len(), 2);
|
||||
assert_eq!(donation_urls[0].url, "https://www.buymeacoffee.com/my_user");
|
||||
assert_eq!(donation_urls[1].url, "https://www.patreon.com/my_user");
|
||||
assert_eq!(alpha_body.issues_url, None);
|
||||
assert_eq!(
|
||||
alpha_body.discord_url,
|
||||
Some("https://discord.gg".to_string())
|
||||
);
|
||||
|
||||
let beta_body = api
|
||||
.get_project_deserialized(beta_project_id, ADMIN_USER_PAT)
|
||||
.await;
|
||||
let donation_urls = beta_body
|
||||
.donation_urls
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.sorted_by_key(|x| x.id.clone())
|
||||
.collect_vec();
|
||||
assert_eq!(donation_urls.len(), 2);
|
||||
assert_eq!(donation_urls[0].url, "https://www.buymeacoffee.com/my_user");
|
||||
assert_eq!(donation_urls[1].url, "https://www.patreon.com/my_user");
|
||||
assert_eq!(alpha_body.issues_url, None);
|
||||
assert_eq!(
|
||||
alpha_body.discord_url,
|
||||
Some("https://discord.gg".to_string())
|
||||
);
|
||||
|
||||
let resp = api
|
||||
.edit_project_bulk(
|
||||
&[alpha_project_id, beta_project_id],
|
||||
json!({
|
||||
"donation_urls": [
|
||||
{
|
||||
"id": "patreon",
|
||||
"platform": "Patreon",
|
||||
"url": "https://www.patreon.com/my_user"
|
||||
},
|
||||
{
|
||||
"id": "ko-fi",
|
||||
"platform": "Ko-fi",
|
||||
"url": "https://www.ko-fi.com/my_user"
|
||||
}
|
||||
],
|
||||
"add_donation_urls": [
|
||||
{
|
||||
"id": "paypal",
|
||||
"platform": "PayPal",
|
||||
"url": "https://www.paypal.com/my_user"
|
||||
}
|
||||
],
|
||||
"remove_donation_urls": [
|
||||
{
|
||||
"id": "ko-fi",
|
||||
"platform": "Ko-fi",
|
||||
"url": "https://www.ko-fi.com/my_user"
|
||||
}
|
||||
],
|
||||
}),
|
||||
ADMIN_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let alpha_body = api
|
||||
.get_project_deserialized(alpha_project_id, ADMIN_USER_PAT)
|
||||
.await;
|
||||
let donation_urls = alpha_body
|
||||
.donation_urls
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.sorted_by_key(|x| x.id.clone())
|
||||
.collect_vec();
|
||||
assert_eq!(donation_urls.len(), 2);
|
||||
assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user");
|
||||
assert_eq!(donation_urls[1].url, "https://www.paypal.com/my_user");
|
||||
|
||||
let beta_body = api
|
||||
.get_project_deserialized(beta_project_id, ADMIN_USER_PAT)
|
||||
.await;
|
||||
let donation_urls = beta_body
|
||||
.donation_urls
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.sorted_by_key(|x| x.id.clone())
|
||||
.collect_vec();
|
||||
assert_eq!(donation_urls.len(), 2);
|
||||
assert_eq!(donation_urls[0].url, "https://www.patreon.com/my_user");
|
||||
assert_eq!(donation_urls[1].url, "https://www.paypal.com/my_user");
|
||||
})
|
||||
.await;
|
||||
}
|
||||
81
apps/labrinth/tests/v2/scopes.rs
Normal file
81
apps/labrinth/tests/v2/scopes.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
use crate::common::api_common::ApiProject;
|
||||
use crate::common::api_common::ApiVersion;
|
||||
use crate::common::api_v2::request_data::get_public_project_creation_data;
|
||||
use crate::common::api_v2::ApiV2;
|
||||
use crate::common::dummy_data::TestFile;
|
||||
use crate::common::environment::with_test_environment;
|
||||
use crate::common::environment::TestEnvironment;
|
||||
use crate::common::scopes::ScopeTest;
|
||||
use labrinth::models::ids::base62_impl::parse_base62;
|
||||
use labrinth::models::pats::Scopes;
|
||||
use labrinth::models::projects::ProjectId;
|
||||
|
||||
// Project version creation scopes
|
||||
#[actix_rt::test]
|
||||
pub async fn project_version_create_scopes() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
|
||||
let api = &test_env.api;
|
||||
// Create project
|
||||
let create_project = Scopes::PROJECT_CREATE;
|
||||
|
||||
let req_gen = |pat: Option<String>| async move {
|
||||
let creation_data =
|
||||
get_public_project_creation_data("demo", Some(TestFile::BasicMod), None);
|
||||
api.create_project(creation_data, pat.as_deref()).await
|
||||
};
|
||||
let (_, success) = ScopeTest::new(&test_env)
|
||||
.test(req_gen, create_project)
|
||||
.await
|
||||
.unwrap();
|
||||
let project_id = success["id"].as_str().unwrap();
|
||||
let project_id = ProjectId(parse_base62(project_id).unwrap());
|
||||
|
||||
// Add version to project
|
||||
let create_version = Scopes::VERSION_CREATE;
|
||||
let req_gen = |pat: Option<String>| async move {
|
||||
api.add_public_version(
|
||||
project_id,
|
||||
"1.2.3.4",
|
||||
TestFile::BasicModDifferent,
|
||||
None,
|
||||
None,
|
||||
pat.as_deref(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
ScopeTest::new(&test_env)
|
||||
.test(req_gen, create_version)
|
||||
.await
|
||||
.unwrap();
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn project_version_reads_scopes() {
|
||||
with_test_environment(None, |_test_env: TestEnvironment<ApiV2>| async move {
|
||||
// let api = &test_env.api;
|
||||
// let beta_file_hash = &test_env.dummy.project_beta.file_hash;
|
||||
|
||||
// let read_version = Scopes::VERSION_READ;
|
||||
|
||||
// Update individual version file
|
||||
// TODO: This scope currently fails still as the 'version' field of QueryProject only allows public versions.
|
||||
// TODO: This will be fixed when the 'extracts_versions' PR is merged.
|
||||
// let req_gen = |pat : Option<String>| async move {
|
||||
// api.update_individual_files("sha1", vec![
|
||||
// FileUpdateData {
|
||||
// hash: beta_file_hash.clone(),
|
||||
// loaders: None,
|
||||
// game_versions: None,
|
||||
// version_types: None
|
||||
// }
|
||||
// ], pat.as_deref())
|
||||
// .await
|
||||
// };
|
||||
// let (failure, success) = ScopeTest::new(&test_env).with_failure_code(200).test(req_gen, read_version).await.unwrap();
|
||||
// assert!(!failure.as_object().unwrap().contains_key(beta_file_hash));
|
||||
// assert!(success.as_object().unwrap().contains_key(beta_file_hash));
|
||||
})
|
||||
.await;
|
||||
}
|
||||
408
apps/labrinth/tests/v2/search.rs
Normal file
408
apps/labrinth/tests/v2/search.rs
Normal file
@@ -0,0 +1,408 @@
|
||||
use crate::assert_status;
|
||||
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 actix_http::StatusCode;
|
||||
use futures::stream::StreamExt;
|
||||
use labrinth::models::ids::base62_impl::parse_base62;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn search_projects() {
|
||||
// Test setup and dummy data
|
||||
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![];
|
||||
|
||||
let create_async_future =
|
||||
|id: u64,
|
||||
pat: Option<&'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()
|
||||
};
|
||||
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_status!(&resp, StatusCode::NO_CONTENT);
|
||||
(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": "/server_side", "value": "required" },
|
||||
{ "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
USER_USER_PAT,
|
||||
false,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Test project 1
|
||||
let id = 1;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] },
|
||||
{ "op": "add", "path": "/client_side", "value": "optional" },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
USER_USER_PAT,
|
||||
false,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Test project 2
|
||||
let id = 2;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..2] },
|
||||
{ "op": "add", "path": "/server_side", "value": "required" },
|
||||
{ "op": "add", "path": "/title", "value": "Mysterious Project" },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
USER_USER_PAT,
|
||||
false,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Test project 3
|
||||
let id = 3;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] },
|
||||
{ "op": "add", "path": "/server_side", "value": "required" },
|
||||
{ "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.4"] },
|
||||
{ "op": "add", "path": "/title", "value": "Mysterious Project" },
|
||||
{ "op": "add", "path": "/license_id", "value": "LicenseRef-All-Rights-Reserved" },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
FRIEND_USER_PAT,
|
||||
false,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Test project 4
|
||||
let id = 4;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[0..3] },
|
||||
{ "op": "add", "path": "/client_side", "value": "optional" },
|
||||
{ "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.5"] },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
USER_USER_PAT,
|
||||
true,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Test project 5
|
||||
let id = 5;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] },
|
||||
{ "op": "add", "path": "/client_side", "value": "optional" },
|
||||
{ "op": "add", "path": "/initial_versions/0/game_versions", "value": ["1.20.5"] },
|
||||
{ "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
USER_USER_PAT,
|
||||
false,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Test project 6
|
||||
let id = 6;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/categories", "value": DUMMY_CATEGORIES[5..6] },
|
||||
{ "op": "add", "path": "/client_side", "value": "optional" },
|
||||
{ "op": "add", "path": "/server_side", "value": "required" },
|
||||
{ "op": "add", "path": "/license_id", "value": "LGPL-3.0-or-later" },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
FRIEND_USER_PAT,
|
||||
false,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Test project 7 (testing the search bug)
|
||||
// This project has an initial private forge version that is 1.20.2, and a fabric 1.20.1 version.
|
||||
// This means that a search for fabric + 1.20.1 or forge + 1.20.1 should not return this project,
|
||||
// but a search for fabric + 1.20.1 should, and it should include both versions in the data.
|
||||
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 8
|
||||
// Server side unsupported
|
||||
let id = 8;
|
||||
let modify_json = serde_json::from_value(json!([
|
||||
{ "op": "add", "path": "/server_side", "value": "unsupported" },
|
||||
]))
|
||||
.unwrap();
|
||||
project_creation_futures.push(create_async_future(
|
||||
id,
|
||||
USER_USER_PAT,
|
||||
false,
|
||||
Some(modify_json),
|
||||
));
|
||||
|
||||
// Await all project creation
|
||||
// Returns a mapping of:
|
||||
// project id -> test id
|
||||
let id_conversion: Arc<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(
|
||||
project_7.id,
|
||||
"1.0.0",
|
||||
TestFile::build_random_jar(),
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Pairs of:
|
||||
// 1. vec of search facets
|
||||
// 2. expected project ids to be returned by this search
|
||||
let pairs = vec![
|
||||
(
|
||||
json!([["categories:fabric"]]),
|
||||
vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
|
||||
),
|
||||
(json!([["categories:forge"]]), vec![7]),
|
||||
(
|
||||
json!([["categories:fabric", "categories:forge"]]),
|
||||
vec![0, 1, 2, 3, 4, 5, 6, 7, 8],
|
||||
),
|
||||
(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]),
|
||||
// Formerly included 7, but with v2 changes, this is no longer the case.
|
||||
// This is because we assume client_side/server_side with subsequent versions.
|
||||
(json!([["client_side:required"]]), vec![0, 2, 3, 8]),
|
||||
(json!([["server_side:required"]]), vec![0, 2, 3, 6, 7]),
|
||||
(json!([["open_source:true"]]), vec![0, 1, 2, 4, 5, 6, 7, 8]),
|
||||
(json!([["license:MIT"]]), vec![1, 2, 4, 8]),
|
||||
(json!([[r#"title:'Mysterious Project'"#]]), vec![2, 3]),
|
||||
(json!([["author:user"]]), vec![0, 1, 2, 4, 5, 7, 8]),
|
||||
(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![],
|
||||
),
|
||||
(
|
||||
json!([
|
||||
// But it does have a 1.20.2 forge version, so this should return it.
|
||||
["categories:forge"],
|
||||
["versions:1.20.2"]
|
||||
]),
|
||||
vec![7],
|
||||
),
|
||||
// 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],
|
||||
),
|
||||
(
|
||||
json!([["client_side:optional"], ["server_side:optional"]]),
|
||||
vec![1, 4, 5],
|
||||
),
|
||||
(json!([["server_side:optional"]]), vec![1, 4, 5]),
|
||||
(json!([["server_side:unsupported"]]), vec![8]),
|
||||
];
|
||||
|
||||
// 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_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// 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(&format!("\"&{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();
|
||||
println!("Facets: {:?}", facets);
|
||||
assert_eq!(found_project_ids, expected_project_ids);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
// A couple additional tests for the search type returned, making sure it is properly translated back
|
||||
let client_side_required = api
|
||||
.search_deserialized(
|
||||
Some(&format!("\"&{test_name}\"")),
|
||||
Some(json!([["client_side:required"]])),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
for hit in client_side_required.hits {
|
||||
assert_eq!(hit.client_side, "required".to_string());
|
||||
}
|
||||
|
||||
let server_side_required = api
|
||||
.search_deserialized(
|
||||
Some(&format!("\"&{test_name}\"")),
|
||||
Some(json!([["server_side:required"]])),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
for hit in server_side_required.hits {
|
||||
assert_eq!(hit.server_side, "required".to_string());
|
||||
}
|
||||
|
||||
let client_side_unsupported = api
|
||||
.search_deserialized(
|
||||
Some(&format!("\"&{test_name}\"")),
|
||||
Some(json!([["client_side:unsupported"]])),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
for hit in client_side_unsupported.hits {
|
||||
assert_eq!(hit.client_side, "unsupported".to_string());
|
||||
}
|
||||
|
||||
let client_side_optional_server_side_optional = api
|
||||
.search_deserialized(
|
||||
Some(&format!("\"&{test_name}\"")),
|
||||
Some(json!([["client_side:optional"], ["server_side:optional"]])),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
for hit in client_side_optional_server_side_optional.hits {
|
||||
assert_eq!(hit.client_side, "optional".to_string());
|
||||
assert_eq!(hit.server_side, "optional".to_string());
|
||||
}
|
||||
|
||||
// Ensure game_versions return correctly, but also correctly aggregated
|
||||
// over all versions of a project
|
||||
let game_versions = api
|
||||
.search_deserialized(
|
||||
Some(&format!("\"&{test_name}\"")),
|
||||
Some(json!([["categories:forge"], ["versions:1.20.2"]])),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(game_versions.hits.len(), 1);
|
||||
for hit in game_versions.hits {
|
||||
assert_eq!(
|
||||
hit.versions,
|
||||
vec!["1.20.1".to_string(), "1.20.2".to_string()]
|
||||
);
|
||||
assert!(hit.categories.contains(&"forge".to_string()));
|
||||
assert!(hit.categories.contains(&"fabric".to_string()));
|
||||
assert!(hit.display_categories.contains(&"forge".to_string()));
|
||||
assert!(hit.display_categories.contains(&"fabric".to_string()));
|
||||
|
||||
// Also, ensure author is correctly capitalized
|
||||
assert_eq!(hit.author, "User".to_string());
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
107
apps/labrinth/tests/v2/tags.rs
Normal file
107
apps/labrinth/tests/v2/tags.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use itertools::Itertools;
|
||||
use labrinth::routes::v2::tags::DonationPlatformQueryData;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::common::{
|
||||
api_v2::ApiV2,
|
||||
environment::{with_test_environment, TestEnvironment},
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn get_tags() {
|
||||
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;
|
||||
|
||||
// These tests match dummy data and will need to be updated if the dummy data changes
|
||||
// Versions should be ordered by:
|
||||
// - ordering
|
||||
// - ordering ties settled by date added to database
|
||||
// - We also expect presentation of NEWEST to OLDEST
|
||||
// - All null orderings are treated as older than any non-null ordering
|
||||
// (for this test, the 1.20.1, etc, versions are all null ordering)
|
||||
let game_version_versions = game_versions
|
||||
.into_iter()
|
||||
.map(|x| x.version)
|
||||
.collect::<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"
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect_vec()
|
||||
);
|
||||
|
||||
let loader_names = loaders.into_iter().map(|x| x.name).collect::<HashSet<_>>();
|
||||
assert_eq!(
|
||||
loader_names,
|
||||
["fabric", "forge", "bukkit", "waterfall"]
|
||||
.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()
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn get_donation_platforms() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
|
||||
let api = &test_env.api;
|
||||
let mut donation_platforms_unsorted = api.get_donation_platforms_deserialized().await;
|
||||
|
||||
// These tests match dummy data and will need to be updated if the dummy data changes
|
||||
let mut included = vec![
|
||||
DonationPlatformQueryData {
|
||||
short: "patreon".to_string(),
|
||||
name: "Patreon".to_string(),
|
||||
},
|
||||
DonationPlatformQueryData {
|
||||
short: "ko-fi".to_string(),
|
||||
name: "Ko-fi".to_string(),
|
||||
},
|
||||
DonationPlatformQueryData {
|
||||
short: "paypal".to_string(),
|
||||
name: "PayPal".to_string(),
|
||||
},
|
||||
DonationPlatformQueryData {
|
||||
short: "bmac".to_string(),
|
||||
name: "Buy Me A Coffee".to_string(),
|
||||
},
|
||||
DonationPlatformQueryData {
|
||||
short: "github".to_string(),
|
||||
name: "GitHub Sponsors".to_string(),
|
||||
},
|
||||
DonationPlatformQueryData {
|
||||
short: "other".to_string(),
|
||||
name: "Other".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
included.sort_by(|a, b| a.short.cmp(&b.short));
|
||||
donation_platforms_unsorted.sort_by(|a, b| a.short.cmp(&b.short));
|
||||
|
||||
assert_eq!(donation_platforms_unsorted, included);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
110
apps/labrinth/tests/v2/teams.rs
Normal file
110
apps/labrinth/tests/v2/teams.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use actix_http::StatusCode;
|
||||
use labrinth::models::teams::ProjectPermissions;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
assert_status,
|
||||
common::{
|
||||
api_common::ApiTeams,
|
||||
api_v2::ApiV2,
|
||||
database::{
|
||||
FRIEND_USER_ID, FRIEND_USER_ID_PARSED, FRIEND_USER_PAT, USER_USER_ID_PARSED,
|
||||
USER_USER_PAT,
|
||||
},
|
||||
environment::{with_test_environment, TestEnvironment},
|
||||
},
|
||||
};
|
||||
|
||||
// trasnfer ownership (requires being owner, etc)
|
||||
#[actix_rt::test]
|
||||
async fn transfer_ownership_v2() {
|
||||
// Test setup and dummy data
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
|
||||
let api = &test_env.api;
|
||||
|
||||
let alpha_team_id = &test_env.dummy.project_alpha.team_id;
|
||||
|
||||
// Cannot set friend as owner (not a member)
|
||||
let resp = api
|
||||
.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// first, invite friend
|
||||
let resp = api
|
||||
.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// still cannot set friend as owner (not accepted)
|
||||
let resp = api
|
||||
.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
|
||||
// accept
|
||||
let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// Cannot set ourselves as owner if we are not owner
|
||||
let resp = api
|
||||
.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, FRIEND_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::UNAUTHORIZED);
|
||||
|
||||
// Can set friend as owner
|
||||
let resp = api
|
||||
.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
// Check
|
||||
let members = api
|
||||
.get_team_members_deserialized(alpha_team_id, USER_USER_PAT)
|
||||
.await;
|
||||
let friend_member = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(friend_member.role, "Owner");
|
||||
assert_eq!(
|
||||
friend_member.permissions.unwrap(),
|
||||
ProjectPermissions::all()
|
||||
);
|
||||
|
||||
let user_member = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(user_member.role, "Member");
|
||||
assert_eq!(user_member.permissions.unwrap(), ProjectPermissions::all());
|
||||
|
||||
// Confirm that user, a user who still has full permissions, cannot then remove the owner
|
||||
let resp = api
|
||||
.remove_from_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::UNAUTHORIZED);
|
||||
|
||||
// V2 only- confirm the owner changing the role to member does nothing
|
||||
let resp = api
|
||||
.edit_team_member(
|
||||
alpha_team_id,
|
||||
FRIEND_USER_ID,
|
||||
json!({
|
||||
"role": "Member"
|
||||
}),
|
||||
FRIEND_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
let members = api
|
||||
.get_team_members_deserialized(alpha_team_id, USER_USER_PAT)
|
||||
.await;
|
||||
let friend_member = members
|
||||
.iter()
|
||||
.find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64)
|
||||
.unwrap();
|
||||
assert_eq!(friend_member.role, "Owner");
|
||||
})
|
||||
.await;
|
||||
}
|
||||
517
apps/labrinth/tests/v2/version.rs
Normal file
517
apps/labrinth/tests/v2/version.rs
Normal file
@@ -0,0 +1,517 @@
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::test;
|
||||
use futures::StreamExt;
|
||||
use labrinth::models::projects::VersionId;
|
||||
use labrinth::{
|
||||
models::projects::{Loader, VersionStatus, VersionType},
|
||||
routes::v2::version_file::FileUpdateData,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::assert_status;
|
||||
use crate::common::api_common::{ApiProject, ApiVersion};
|
||||
use crate::common::api_v2::ApiV2;
|
||||
|
||||
use crate::common::api_v2::request_data::get_public_project_creation_data;
|
||||
use crate::common::dummy_data::{DummyProjectAlpha, DummyProjectBeta};
|
||||
use crate::common::environment::{with_test_environment, TestEnvironment};
|
||||
use crate::common::{
|
||||
database::{ENEMY_USER_PAT, USER_USER_PAT},
|
||||
dummy_data::TestFile,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn test_patch_version() {
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
|
||||
let api = &test_env.api;
|
||||
|
||||
let alpha_version_id = &test_env.dummy.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_status!(&resp, StatusCode::UNAUTHORIZED);
|
||||
|
||||
// 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_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 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_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
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_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
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_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
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
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
|
||||
let api = &test_env.api;
|
||||
let DummyProjectAlpha {
|
||||
project_id: alpha_project_id,
|
||||
project_id_parsed: alpha_project_id_parsed,
|
||||
version_id: alpha_version_id,
|
||||
file_hash: alpha_version_hash,
|
||||
..
|
||||
} = &test_env.dummy.project_alpha;
|
||||
let DummyProjectBeta {
|
||||
version_id: beta_version_id,
|
||||
file_hash: beta_version_hash,
|
||||
..
|
||||
} = &test_env.dummy.project_beta;
|
||||
|
||||
// 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_common(
|
||||
alpha_version_hash,
|
||||
"sha1",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(&version.id.to_string(), alpha_version_id);
|
||||
|
||||
let versions = api
|
||||
.update_files_deserialized_common(
|
||||
"sha1",
|
||||
vec![alpha_version_hash.to_string()],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(versions.len(), 1);
|
||||
assert_eq!(
|
||||
&versions[alpha_version_hash].id.to_string(),
|
||||
alpha_version_id
|
||||
);
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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_status!(&resp, StatusCode::OK);
|
||||
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_status!(&resp, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// 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, "project"); // No project_type set, as no versions are set
|
||||
// Default to 'project' if none are found
|
||||
// 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;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_incorrect_file_parts() {
|
||||
// Ensures that a version get that 'should' have mrpack_loaders does still display them
|
||||
// if the file is 'mrpack' but the file_parts are incorrect
|
||||
with_test_environment(None, |test_env: TestEnvironment<ApiV2>| async move {
|
||||
let api = &test_env.api;
|
||||
|
||||
// Patch to set the file_parts to something incorrect
|
||||
let patch = json!([{
|
||||
"op": "add",
|
||||
"path": "/file_parts",
|
||||
"value": ["invalid.zip"] // one file, wrong non-mrpack extension
|
||||
}]);
|
||||
|
||||
// Create an empty project
|
||||
let slug = "test-project";
|
||||
let creation_data = get_public_project_creation_data(slug, None, None);
|
||||
let resp = api.create_project(creation_data, USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
// Get the project
|
||||
let project = api.get_project_deserialized(slug, USER_USER_PAT).await;
|
||||
assert_eq!(project.project_type, "project");
|
||||
|
||||
// Create a version with a mrpack file, but incorrect file_parts
|
||||
let resp = api
|
||||
.add_public_version(
|
||||
project.id,
|
||||
"1.0.0",
|
||||
TestFile::build_random_mrpack(),
|
||||
None,
|
||||
Some(serde_json::from_value(patch).unwrap()),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
|
||||
// Get the project now, which should be now correctly identified as a modpack
|
||||
let project = api.get_project_deserialized(slug, USER_USER_PAT).await;
|
||||
assert_eq!(project.project_type, "modpack");
|
||||
assert_eq!(project.loaders, vec!["fabric"]);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
19
apps/labrinth/tests/v2_tests.rs
Normal file
19
apps/labrinth/tests/v2_tests.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
// importing common module.
|
||||
mod common;
|
||||
|
||||
// Not all tests expect exactly the same functionality in v2 and v3.
|
||||
// For example, though we expect the /GET version to return the corresponding project,
|
||||
// we may want to do different checks for each.
|
||||
// (such as checking client_side in v2, but loader fields on v3- which are model-exclusie)
|
||||
|
||||
// Such V2 tests are exported here
|
||||
mod v2 {
|
||||
mod error;
|
||||
mod notifications;
|
||||
mod project;
|
||||
mod scopes;
|
||||
mod search;
|
||||
mod tags;
|
||||
mod teams;
|
||||
mod version;
|
||||
}
|
||||
689
apps/labrinth/tests/version.rs
Normal file
689
apps/labrinth/tests/version.rs
Normal file
@@ -0,0 +1,689 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::common::api_common::ApiVersion;
|
||||
use crate::common::database::*;
|
||||
use crate::common::dummy_data::{DummyProjectAlpha, DummyProjectBeta, TestFile};
|
||||
use crate::common::get_json_val_str;
|
||||
use actix_http::StatusCode;
|
||||
use actix_web::test;
|
||||
use common::api_v3::ApiV3;
|
||||
use common::asserts::assert_common_version_ids;
|
||||
use common::database::USER_USER_PAT;
|
||||
use common::environment::{with_test_environment, with_test_environment_all};
|
||||
use futures::StreamExt;
|
||||
use labrinth::database::models::version_item::VERSIONS_NAMESPACE;
|
||||
use labrinth::models::ids::base62_impl::parse_base62;
|
||||
use labrinth::models::projects::{
|
||||
Dependency, DependencyType, VersionId, VersionStatus, VersionType,
|
||||
};
|
||||
use labrinth::routes::v3::version_file::FileUpdateData;
|
||||
use serde_json::json;
|
||||
|
||||
// importing common module.
|
||||
mod common;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_get_version() {
|
||||
// Test setup and dummy data
|
||||
with_test_environment_all(None, |test_env| async move {
|
||||
let api = &test_env.api;
|
||||
let DummyProjectAlpha {
|
||||
project_id: alpha_project_id,
|
||||
version_id: alpha_version_id,
|
||||
..
|
||||
} = &test_env.dummy.project_alpha;
|
||||
let DummyProjectBeta {
|
||||
version_id: beta_version_id,
|
||||
..
|
||||
} = &test_env.dummy.project_beta;
|
||||
|
||||
// Perform request on dummy data
|
||||
let version = api
|
||||
.get_version_deserialized_common(alpha_version_id, USER_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(&version.project_id.to_string(), alpha_project_id);
|
||||
assert_eq!(&version.id.to_string(), alpha_version_id);
|
||||
|
||||
let mut redis_conn = test_env.db.redis_pool.connect().await.unwrap();
|
||||
let cached_project = redis_conn
|
||||
.get(
|
||||
VERSIONS_NAMESPACE,
|
||||
&parse_base62(alpha_version_id).unwrap().to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let cached_project: serde_json::Value = serde_json::from_str(&cached_project).unwrap();
|
||||
assert_eq!(
|
||||
cached_project["val"]["inner"]["project_id"],
|
||||
json!(parse_base62(alpha_project_id).unwrap())
|
||||
);
|
||||
|
||||
// Request should fail on non-existent version
|
||||
let resp = api.get_version("false", USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NOT_FOUND);
|
||||
|
||||
// Similarly, request should fail on non-authorized user, on a yet-to-be-approved or hidden project, with a 404 (hiding the existence of the project)
|
||||
// TODO: beta version should already be draft in dummy data, but theres a bug in finding it that
|
||||
api.edit_version(
|
||||
beta_version_id,
|
||||
json!({
|
||||
"status": "draft"
|
||||
}),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
let resp = api.get_version(beta_version_id, USER_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::OK);
|
||||
let resp = api.get_version(beta_version_id, ENEMY_USER_PAT).await;
|
||||
assert_status!(&resp, StatusCode::NOT_FOUND);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn version_updates() {
|
||||
// Test setup and dummy data
|
||||
with_test_environment(
|
||||
None,
|
||||
|test_env: common::environment::TestEnvironment<ApiV3>| async move {
|
||||
let api = &test_env.api;
|
||||
let DummyProjectAlpha {
|
||||
project_id: alpha_project_id,
|
||||
project_id_parsed: alpha_project_id_parsed,
|
||||
version_id: alpha_version_id,
|
||||
file_hash: alpha_version_hash,
|
||||
..
|
||||
} = &test_env.dummy.project_alpha;
|
||||
let DummyProjectBeta {
|
||||
version_id: beta_version_id,
|
||||
file_hash: beta_version_hash,
|
||||
..
|
||||
} = &test_env.dummy.project_beta;
|
||||
|
||||
// Quick test, using get version from hash
|
||||
let version = api
|
||||
.get_version_from_hash_deserialized_common(
|
||||
alpha_version_hash,
|
||||
"sha1",
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(&version.id.to_string(), alpha_version_id);
|
||||
|
||||
// Get versions from hash
|
||||
let versions = api
|
||||
.get_versions_from_hashes_deserialized_common(
|
||||
&[alpha_version_hash.as_str(), beta_version_hash.as_str()],
|
||||
"sha1",
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(versions.len(), 2);
|
||||
assert_eq!(
|
||||
&versions[alpha_version_hash].id.to_string(),
|
||||
alpha_version_id
|
||||
);
|
||||
assert_eq!(&versions[beta_version_hash].id.to_string(), beta_version_id);
|
||||
|
||||
// When there is only the one version, there should be no updates
|
||||
let version = api
|
||||
.get_update_from_hash_deserialized_common(
|
||||
alpha_version_hash,
|
||||
"sha1",
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(&version.id.to_string(), alpha_version_id);
|
||||
|
||||
let versions = api
|
||||
.update_files_deserialized_common(
|
||||
"sha1",
|
||||
vec![alpha_version_hash.to_string()],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(versions.len(), 1);
|
||||
assert_eq!(
|
||||
&versions[alpha_version_hash].id.to_string(),
|
||||
alpha_version_id
|
||||
);
|
||||
|
||||
// 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(
|
||||
*alpha_project_id_parsed,
|
||||
version_number,
|
||||
TestFile::build_random_jar(),
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
update_ids.push(version.id);
|
||||
|
||||
// Patch using json
|
||||
api.edit_version(&version.id.to_string(), patch_value.clone(), USER_USER_PAT)
|
||||
.await;
|
||||
}
|
||||
|
||||
let check_expected = |game_versions: Option<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_status!(&resp, StatusCode::OK);
|
||||
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_status!(&resp, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
// update_files
|
||||
let versions = api
|
||||
.update_files_deserialized_common(
|
||||
"sha1",
|
||||
vec![alpha_version_hash.to_string()],
|
||||
loaders.clone(),
|
||||
game_versions.clone(),
|
||||
version_types.clone(),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
if success {
|
||||
assert_eq!(versions.len(), 1);
|
||||
let first = versions.iter().next().unwrap();
|
||||
assert_eq!(first.1.id, result_id);
|
||||
} else {
|
||||
assert_eq!(versions.len(), 0);
|
||||
}
|
||||
|
||||
// update_individual_files
|
||||
let mut loader_fields = HashMap::new();
|
||||
if let Some(game_versions) = game_versions {
|
||||
loader_fields.insert(
|
||||
"game_versions".to_string(),
|
||||
game_versions
|
||||
.into_iter()
|
||||
.map(|v| json!(v))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
let hashes = vec![FileUpdateData {
|
||||
hash: alpha_version_hash.to_string(),
|
||||
loaders,
|
||||
loader_fields: Some(loader_fields),
|
||||
version_types: version_types.map(|v| {
|
||||
v.into_iter()
|
||||
.map(|v| serde_json::from_str(&format!("\"{v}\"")).unwrap())
|
||||
.collect()
|
||||
}),
|
||||
}];
|
||||
let versions = api
|
||||
.update_individual_files_deserialized("sha1", hashes, USER_USER_PAT)
|
||||
.await;
|
||||
if success {
|
||||
assert_eq!(versions.len(), 1);
|
||||
let first = versions.iter().next().unwrap();
|
||||
assert_eq!(first.1.id, result_id);
|
||||
} else {
|
||||
assert_eq!(versions.len(), 0);
|
||||
}
|
||||
};
|
||||
|
||||
let tests = vec![
|
||||
check_expected(
|
||||
Some(vec!["1.20.1".to_string()]),
|
||||
None,
|
||||
None,
|
||||
Some(update_ids[0]),
|
||||
),
|
||||
check_expected(
|
||||
Some(vec!["1.20.3".to_string()]),
|
||||
None,
|
||||
None,
|
||||
Some(update_ids[1]),
|
||||
),
|
||||
check_expected(
|
||||
Some(vec!["1.20.4".to_string()]),
|
||||
None,
|
||||
None,
|
||||
Some(update_ids[2]),
|
||||
),
|
||||
// Loader restrictions
|
||||
check_expected(
|
||||
None,
|
||||
Some(vec!["fabric".to_string()]),
|
||||
None,
|
||||
Some(update_ids[1]),
|
||||
),
|
||||
check_expected(
|
||||
None,
|
||||
Some(vec!["forge".to_string()]),
|
||||
None,
|
||||
Some(update_ids[2]),
|
||||
),
|
||||
// Version type restrictions
|
||||
check_expected(
|
||||
None,
|
||||
None,
|
||||
Some(vec!["release".to_string()]),
|
||||
Some(update_ids[1]),
|
||||
),
|
||||
check_expected(
|
||||
None,
|
||||
None,
|
||||
Some(vec!["beta".to_string()]),
|
||||
Some(update_ids[2]),
|
||||
),
|
||||
// Specific combination
|
||||
check_expected(
|
||||
None,
|
||||
Some(vec!["fabric".to_string()]),
|
||||
Some(vec!["release".to_string()]),
|
||||
Some(update_ids[1]),
|
||||
),
|
||||
// Impossible combination
|
||||
check_expected(
|
||||
None,
|
||||
Some(vec!["fabric".to_string()]),
|
||||
Some(vec!["beta".to_string()]),
|
||||
None,
|
||||
),
|
||||
// No restrictions, should do the last one
|
||||
check_expected(None, None, None, Some(update_ids[2])),
|
||||
];
|
||||
|
||||
// Wait on all tests, 4 at a time
|
||||
futures::stream::iter(tests)
|
||||
.buffer_unordered(4)
|
||||
.collect::<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_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]
|
||||
pub async fn test_patch_version() {
|
||||
with_test_environment_all(None, |test_env| async move {
|
||||
let api = &test_env.api;
|
||||
|
||||
let alpha_version_id = &test_env.dummy.project_alpha.version_id;
|
||||
let DummyProjectBeta {
|
||||
project_id: beta_project_id,
|
||||
project_id_parsed: beta_project_id_parsed,
|
||||
..
|
||||
} = &test_env.dummy.project_beta;
|
||||
|
||||
// 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_status!(&resp, StatusCode::UNAUTHORIZED);
|
||||
|
||||
// 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_status!(&resp, StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
// 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": [{
|
||||
"project_id": beta_project_id,
|
||||
"dependency_type": "required",
|
||||
"file_name": "dummy_file_name"
|
||||
}],
|
||||
"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_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let version = api
|
||||
.get_version_deserialized_common(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.dependencies,
|
||||
vec![Dependency {
|
||||
project_id: Some(*beta_project_id_parsed),
|
||||
version_id: None,
|
||||
file_name: Some("dummy_file_name".to_string()),
|
||||
dependency_type: DependencyType::Required
|
||||
}]
|
||||
);
|
||||
assert_eq!(version.loaders, vec!["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_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let version = api
|
||||
.get_version_deserialized_common(alpha_version_id, USER_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(version.loaders, vec!["forge".to_string()]); // From last patch
|
||||
|
||||
let resp = api
|
||||
.edit_version(
|
||||
alpha_version_id,
|
||||
json!({
|
||||
"loaders": ["fabric"],
|
||||
}),
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let version = api
|
||||
.get_version_deserialized_common(alpha_version_id, USER_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(version.loaders, vec!["fabric".to_string()]);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn test_project_versions() {
|
||||
with_test_environment_all(None, |test_env| async move {
|
||||
let api = &test_env.api;
|
||||
let alpha_project_id: &String = &test_env.dummy.project_alpha.project_id;
|
||||
let alpha_version_id = &test_env.dummy.project_alpha.version_id;
|
||||
|
||||
let versions = api
|
||||
.get_project_versions_deserialized_common(
|
||||
alpha_project_id,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(versions.len(), 1);
|
||||
assert_eq!(&versions[0].id.to_string(), alpha_version_id);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn can_create_version_with_ordering() {
|
||||
with_test_environment(
|
||||
None,
|
||||
|env: common::environment::TestEnvironment<ApiV3>| async move {
|
||||
let alpha_project_id_parsed = env.dummy.project_alpha.project_id_parsed;
|
||||
|
||||
let new_version_id = get_json_val_str(
|
||||
env.api
|
||||
.add_public_version_deserialized_common(
|
||||
alpha_project_id_parsed,
|
||||
"1.2.3.4",
|
||||
TestFile::BasicMod,
|
||||
Some(1),
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await
|
||||
.id,
|
||||
);
|
||||
|
||||
let versions = env
|
||||
.api
|
||||
.get_versions_deserialized(vec![new_version_id.clone()], USER_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(versions[0].ordering, Some(1));
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn edit_version_ordering_works() {
|
||||
with_test_environment(
|
||||
None,
|
||||
|env: common::environment::TestEnvironment<ApiV3>| async move {
|
||||
let alpha_version_id = env.dummy.project_alpha.version_id.clone();
|
||||
|
||||
let resp = env
|
||||
.api
|
||||
.edit_version_ordering(&alpha_version_id, Some(10), USER_USER_PAT)
|
||||
.await;
|
||||
assert_status!(&resp, StatusCode::NO_CONTENT);
|
||||
|
||||
let versions = env
|
||||
.api
|
||||
.get_versions_deserialized(vec![alpha_version_id.clone()], USER_USER_PAT)
|
||||
.await;
|
||||
assert_eq!(versions[0].ordering, Some(10));
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn version_ordering_for_specified_orderings_orders_lower_order_first() {
|
||||
with_test_environment_all(None, |env| async move {
|
||||
let alpha_project_id_parsed = env.dummy.project_alpha.project_id_parsed;
|
||||
let alpha_version_id = env.dummy.project_alpha.version_id.clone();
|
||||
let new_version_id = get_json_val_str(
|
||||
env.api
|
||||
.add_public_version_deserialized_common(
|
||||
alpha_project_id_parsed,
|
||||
"1.2.3.4",
|
||||
TestFile::BasicMod,
|
||||
Some(1),
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await
|
||||
.id,
|
||||
);
|
||||
env.api
|
||||
.edit_version_ordering(&alpha_version_id, Some(10), USER_USER_PAT)
|
||||
.await;
|
||||
|
||||
let versions = env
|
||||
.api
|
||||
.get_versions_deserialized_common(
|
||||
vec![alpha_version_id.clone(), new_version_id.clone()],
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_common_version_ids(&versions, vec![new_version_id, alpha_version_id]);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn version_ordering_when_unspecified_orders_oldest_first() {
|
||||
with_test_environment_all(None, |env| async move {
|
||||
let alpha_project_id_parsed = env.dummy.project_alpha.project_id_parsed;
|
||||
let alpha_version_id: String = env.dummy.project_alpha.version_id.clone();
|
||||
let new_version_id = get_json_val_str(
|
||||
env.api
|
||||
.add_public_version_deserialized_common(
|
||||
alpha_project_id_parsed,
|
||||
"1.2.3.4",
|
||||
TestFile::BasicMod,
|
||||
None,
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await
|
||||
.id,
|
||||
);
|
||||
|
||||
let versions = env
|
||||
.api
|
||||
.get_versions_deserialized_common(
|
||||
vec![alpha_version_id.clone(), new_version_id.clone()],
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_common_version_ids(&versions, vec![alpha_version_id, new_version_id]);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn version_ordering_when_specified_orders_specified_before_unspecified() {
|
||||
with_test_environment_all(None, |env| async move {
|
||||
let alpha_project_id_parsed = env.dummy.project_alpha.project_id_parsed;
|
||||
let alpha_version_id = env.dummy.project_alpha.version_id.clone();
|
||||
let new_version_id = get_json_val_str(
|
||||
env.api
|
||||
.add_public_version_deserialized_common(
|
||||
alpha_project_id_parsed,
|
||||
"1.2.3.4",
|
||||
TestFile::BasicMod,
|
||||
Some(1000),
|
||||
None,
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await
|
||||
.id,
|
||||
);
|
||||
env.api
|
||||
.edit_version_ordering(&alpha_version_id, None, USER_USER_PAT)
|
||||
.await;
|
||||
|
||||
let versions = env
|
||||
.api
|
||||
.get_versions_deserialized_common(
|
||||
vec![alpha_version_id.clone(), new_version_id.clone()],
|
||||
USER_USER_PAT,
|
||||
)
|
||||
.await;
|
||||
assert_common_version_ids(&versions, vec![new_version_id, alpha_version_id]);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
Reference in New Issue
Block a user