From f939e594638639f5c87d08166f4ef02a549c7301 Mon Sep 17 00:00:00 2001 From: Wyatt Verchere Date: Thu, 14 Dec 2023 15:19:50 -0700 Subject: [PATCH] Testing bug fixes (#788) * fixes * adds tests- fixes failures * changes * moved transaction commits/caches around * collections nullable * merge fixes * sqlx prepare * revs * lf fixes * made changes back * added collections update --------- Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com> --- ...c879b6cb54517aaf36c55b6f99f1604015bb7.json | 14 +++ ...e6c3b401fcaa0bcc304b9caf18afc20a3e52b.json | 22 +++++ ...c93a23895e6b10a4eb34aa761d29acfa24fb0.json | 2 +- ...5095400_remaining_loader_field_loaders.sql | 16 ++++ ...84922_collections_description_nullable.sql | 1 + src/database/models/collection_item.rs | 6 +- src/database/models/project_item.rs | 24 ++++- src/models/v2/projects.rs | 10 +-- src/models/v3/collections.rs | 2 +- src/queue/payouts.rs | 4 +- src/queue/session.rs | 3 +- src/routes/internal/flows.rs | 14 +-- src/routes/internal/pats.rs | 6 +- src/routes/internal/session.rs | 5 +- src/routes/v2/tags.rs | 25 +++++- src/routes/v2/version_creation.rs | 68 ++++++++++---- src/routes/v2_reroute.rs | 8 ++ src/routes/v3/collections.rs | 32 +++++-- src/routes/v3/organizations.rs | 9 +- src/routes/v3/payouts.rs | 26 +++--- src/routes/v3/projects.rs | 20 ++--- src/routes/v3/tags.rs | 6 +- src/routes/v3/teams.rs | 16 ++-- src/routes/v3/users.rs | 2 +- src/routes/v3/versions.rs | 7 +- tests/common/api_common/generic.rs | 2 + tests/common/api_common/mod.rs | 13 ++- tests/common/api_common/models.rs | 22 ++++- tests/common/api_v2/project.rs | 35 +++++++- tests/common/api_v2/tags.rs | 19 +++- tests/common/api_v3/project.rs | 35 +++++++- tests/project.rs | 89 +++++++++++++++++++ tests/v2/tags.rs | 43 +++++++++ 33 files changed, 494 insertions(+), 112 deletions(-) create mode 100644 .sqlx/query-62378074f2f12d010b4b2139ac8c879b6cb54517aaf36c55b6f99f1604015bb7.json create mode 100644 .sqlx/query-902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b.json create mode 100644 migrations/20231205095400_remaining_loader_field_loaders.sql create mode 100644 migrations/20231211184922_collections_description_nullable.sql diff --git a/.sqlx/query-62378074f2f12d010b4b2139ac8c879b6cb54517aaf36c55b6f99f1604015bb7.json b/.sqlx/query-62378074f2f12d010b4b2139ac8c879b6cb54517aaf36c55b6f99f1604015bb7.json new file mode 100644 index 00000000..63d3d400 --- /dev/null +++ b/.sqlx/query-62378074f2f12d010b4b2139ac8c879b6cb54517aaf36c55b6f99f1604015bb7.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE collections\n SET updated = NOW()\n WHERE id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [] + }, + "hash": "62378074f2f12d010b4b2139ac8c879b6cb54517aaf36c55b6f99f1604015bb7" +} diff --git a/.sqlx/query-902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b.json b/.sqlx/query-902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b.json new file mode 100644 index 00000000..9af819ae --- /dev/null +++ b/.sqlx/query-902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT t.id\n FROM threads t\n INNER JOIN reports r ON t.report_id = r.id\n WHERE r.mod_id = $1 AND report_id IS NOT NULL \n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "902d0803deb5eca7614f3a68ccae6c3b401fcaa0bcc304b9caf18afc20a3e52b" +} diff --git a/.sqlx/query-f2f865b1f1428ed9469e8f73796c93a23895e6b10a4eb34aa761d29acfa24fb0.json b/.sqlx/query-f2f865b1f1428ed9469e8f73796c93a23895e6b10a4eb34aa761d29acfa24fb0.json index f117b90b..c1b79a18 100644 --- a/.sqlx/query-f2f865b1f1428ed9469e8f73796c93a23895e6b10a4eb34aa761d29acfa24fb0.json +++ b/.sqlx/query-f2f865b1f1428ed9469e8f73796c93a23895e6b10a4eb34aa761d29acfa24fb0.json @@ -62,7 +62,7 @@ "nullable": [ false, false, - false, + true, true, true, false, diff --git a/migrations/20231205095400_remaining_loader_field_loaders.sql b/migrations/20231205095400_remaining_loader_field_loaders.sql new file mode 100644 index 00000000..6ff87804 --- /dev/null +++ b/migrations/20231205095400_remaining_loader_field_loaders.sql @@ -0,0 +1,16 @@ +-- Adds loader_fields_loaders entries for all loaders +-- (at this point, they are all Minecraft loaders, and thus have the same fields) +-- These are loaders such as bukkit, minecraft, vanilla, waterfall, velocity... etc +-- This also allows v2 routes (which have things such as client_side to remain to work with these loaders) +INSERT INTO loader_fields_loaders +SELECT l.id, lf.id FROM loaders l CROSS JOIN loader_fields lf +WHERE lf.field=ANY(ARRAY['game_versions','client_and_server','server_only','client_only','singleplayer']) +AND +l.loader NOT IN ('vanilla', 'minecraft', 'optifine', 'iris', 'canvas') +ON CONFLICT DO NOTHING; + +-- All existing loader_project_types so far should have a games entry as minecraft +INSERT INTO loaders_project_types_games +SELECT lpt.joining_loader_id, lpt.joining_project_type_id, g.id FROM loaders_project_types lpt CROSS JOIN games g +WHERE g.name='minecraft-java' +ON CONFLICT DO NOTHING; \ No newline at end of file diff --git a/migrations/20231211184922_collections_description_nullable.sql b/migrations/20231211184922_collections_description_nullable.sql new file mode 100644 index 00000000..08b39c3e --- /dev/null +++ b/migrations/20231211184922_collections_description_nullable.sql @@ -0,0 +1 @@ +ALTER TABLE collections ALTER COLUMN description DROP NOT NULL; \ No newline at end of file diff --git a/src/database/models/collection_item.rs b/src/database/models/collection_item.rs index 035fad6b..a2c29283 100644 --- a/src/database/models/collection_item.rs +++ b/src/database/models/collection_item.rs @@ -13,7 +13,7 @@ pub struct CollectionBuilder { pub collection_id: CollectionId, pub user_id: UserId, pub name: String, - pub description: String, + pub description: Option, pub status: CollectionStatus, pub projects: Vec, } @@ -45,7 +45,7 @@ pub struct Collection { pub id: CollectionId, pub user_id: UserId, pub name: String, - pub description: String, + pub description: Option, pub created: DateTime, pub updated: DateTime, pub icon_url: Option, @@ -73,7 +73,7 @@ impl Collection { self.id as CollectionId, self.user_id as UserId, &self.name, - &self.description, + self.description.as_ref(), self.created, self.icon_url.as_ref(), self.status.to_string(), diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index 2cc26a7e..f2e31fce 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -344,6 +344,28 @@ impl Project { .execute(&mut **transaction) .await?; + // Notably joins with report id and not thread.mod_id directly, as + // this is set to null for threads that are reports. + let report_threads = sqlx::query!( + " + SELECT t.id + FROM threads t + INNER JOIN reports r ON t.report_id = r.id + WHERE r.mod_id = $1 AND report_id IS NOT NULL + ", + id as ProjectId, + ) + .fetch_many(&mut **transaction) + .try_filter_map(|e| async { Ok(e.right().map(|x| ThreadId(x.id))) }) + .try_collect::>() + .await?; + + for thread_id in report_threads { + models::Thread::remove_full(thread_id, transaction).await?; + } + + models::Thread::remove_full(project.thread_id, transaction).await?; + sqlx::query!( " DELETE FROM reports @@ -398,8 +420,6 @@ impl Project { .execute(&mut **transaction) .await?; - models::Thread::remove_full(project.thread_id, transaction).await?; - sqlx::query!( " DELETE FROM mods diff --git a/src/models/v2/projects.rs b/src/models/v2/projects.rs index f08af65c..5deb81c9 100644 --- a/src/models/v2/projects.rs +++ b/src/models/v2/projects.rs @@ -14,7 +14,7 @@ use crate::models::projects::{ ProjectStatus, Version, VersionFile, VersionStatus, VersionType, }; use crate::models::threads::ThreadId; -use crate::routes::v2_reroute; +use crate::routes::v2_reroute::{self, capitalize_first}; use chrono::{DateTime, Utc}; use itertools::Itertools; use serde::{Deserialize, Serialize}; @@ -395,11 +395,3 @@ impl TryFrom for DonationLink { }) } } - -fn capitalize_first(input: &str) -> String { - let mut result = input.to_owned(); - if let Some(first_char) = result.get_mut(0..1) { - first_char.make_ascii_uppercase(); - } - result -} diff --git a/src/models/v3/collections.rs b/src/models/v3/collections.rs index 77c74d30..52a937bd 100644 --- a/src/models/v3/collections.rs +++ b/src/models/v3/collections.rs @@ -22,7 +22,7 @@ pub struct Collection { /// The title or name of the collection. pub name: String, /// A short description of the collection. - pub description: String, + pub description: Option, /// An icon URL for the collection. pub icon_url: Option, diff --git a/src/queue/payouts.rs b/src/queue/payouts.rs index 1672ec50..29b85263 100644 --- a/src/queue/payouts.rs +++ b/src/queue/payouts.rs @@ -720,6 +720,8 @@ pub async fn process_payout( .execute(&mut *transaction) .await?; + transaction.commit().await?; + if !clear_cache_users.is_empty() { crate::database::models::User::clear_caches( &clear_cache_users @@ -731,8 +733,6 @@ pub async fn process_payout( .await?; } - transaction.commit().await?; - Ok(()) } diff --git a/src/queue/session.rs b/src/queue/session.rs index e99a7507..2f7b3bfd 100644 --- a/src/queue/session.rs +++ b/src/queue/session.rs @@ -131,11 +131,10 @@ impl AuthQueue { .execute(&mut *transaction) .await?; - PersonalAccessToken::clear_cache(clear_cache_pats, redis).await?; - update_oauth_access_token_last_used(oauth_access_token_queue, &mut transaction).await?; transaction.commit().await?; + PersonalAccessToken::clear_cache(clear_cache_pats, redis).await?; } Ok(()) diff --git a/src/routes/internal/flows.rs b/src/routes/internal/flows.rs index b97e332c..bf831544 100644 --- a/src/routes/internal/flows.rs +++ b/src/routes/internal/flows.rs @@ -1195,8 +1195,8 @@ pub async fn auth_callback( )?; } - crate::database::models::User::clear_caches(&[(id, None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(id, None)], &redis).await?; if let Some(url) = url { Ok(HttpResponse::TemporaryRedirect() @@ -1395,8 +1395,8 @@ pub async fn delete_auth_provider( } } - crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; Ok(HttpResponse::NoContent().finish()) } @@ -1864,8 +1864,8 @@ pub async fn finish_2fa_flow( )?; } - crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; Ok(HttpResponse::Ok().json(serde_json::json!({ "backup_codes": codes, @@ -1952,8 +1952,8 @@ pub async fn remove_2fa( )?; } - crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; Ok(HttpResponse::NoContent().finish()) } @@ -2138,8 +2138,8 @@ pub async fn change_password( )?; } - crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; Ok(HttpResponse::Ok().finish()) } @@ -2210,8 +2210,8 @@ pub async fn set_email( "We need to verify your email address.", )?; - crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id.into(), None)], &redis).await?; Ok(HttpResponse::Ok().finish()) } @@ -2300,8 +2300,8 @@ pub async fn verify_email( .await?; Flow::remove(&email.flow, &redis).await?; - crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; Ok(HttpResponse::NoContent().finish()) } else { diff --git a/src/routes/internal/pats.rs b/src/routes/internal/pats.rs index b8b2d918..64f349e9 100644 --- a/src/routes/internal/pats.rs +++ b/src/routes/internal/pats.rs @@ -127,12 +127,12 @@ pub async fn create_pat( .insert(&mut transaction) .await?; + transaction.commit().await?; database::models::pat_item::PersonalAccessToken::clear_cache( vec![(None, None, Some(user.id.into()))], &redis, ) .await?; - transaction.commit().await?; Ok(HttpResponse::Ok().json(PersonalAccessToken { id: id.into(), @@ -232,12 +232,12 @@ pub async fn edit_pat( .await?; } + transaction.commit().await?; database::models::pat_item::PersonalAccessToken::clear_cache( vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))], &redis, ) .await?; - transaction.commit().await?; } } @@ -269,12 +269,12 @@ pub async fn delete_pat( let mut transaction = pool.begin().await?; database::models::pat_item::PersonalAccessToken::remove(pat.id, &mut transaction) .await?; + transaction.commit().await?; database::models::pat_item::PersonalAccessToken::clear_cache( vec![(Some(pat.id), Some(pat.access_token), Some(pat.user_id))], &redis, ) .await?; - transaction.commit().await?; } } diff --git a/src/routes/internal/session.rs b/src/routes/internal/session.rs index 595bc3e3..e2435e48 100644 --- a/src/routes/internal/session.rs +++ b/src/routes/internal/session.rs @@ -187,6 +187,7 @@ pub async fn delete( if session.user_id == current_user.id.into() { let mut transaction = pool.begin().await?; DBSession::remove(session.id, &mut transaction).await?; + transaction.commit().await?; DBSession::clear_cache( vec![( Some(session.id), @@ -196,7 +197,6 @@ pub async fn delete( &redis, ) .await?; - transaction.commit().await?; } } @@ -232,6 +232,7 @@ pub async fn refresh( DBSession::remove(session.id, &mut transaction).await?; let new_session = issue_session(req, session.user_id, &mut transaction, &redis).await?; + transaction.commit().await?; DBSession::clear_cache( vec![( Some(session.id), @@ -242,8 +243,6 @@ pub async fn refresh( ) .await?; - transaction.commit().await?; - Ok(HttpResponse::Ok().json(Session::from(new_session, true, None))) } else { Err(ApiError::Authentication( diff --git a/src/routes/v2/tags.rs b/src/routes/v2/tags.rs index 69a3eff2..2713d355 100644 --- a/src/routes/v2/tags.rs +++ b/src/routes/v2/tags.rs @@ -4,6 +4,7 @@ use super::ApiError; use crate::database::models::loader_fields::LoaderFieldEnumValue; use crate::database::redis::RedisPool; use crate::models::v2::projects::LegacySideType; +use crate::routes::v2_reroute::capitalize_first; use crate::routes::v3::tags::{ LinkPlatformQueryData, LoaderData as LoaderDataV3, LoaderFieldsEnumQuery, }; @@ -172,11 +173,12 @@ pub async fn license_text(params: web::Path<(String,)>) -> Result { let platforms = platforms .into_iter() - .map(|p| DonationPlatformQueryData { name: p.name }) + .filter_map(|p| { + if p.donation { + Some(DonationPlatformQueryData { + // Short vs name is no longer a recognized difference in v3. + // We capitalize to recreate the old behavior, with some special handling. + // This may result in different behaviour for platforms added after the v3 migration. + name: match p.name.as_str() { + "bmac" => "Buy Me A Coffee".to_string(), + "github" => "GitHub Sponsors".to_string(), + "ko-fi" => "Ko-fi".to_string(), + "paypal" => "PayPal".to_string(), + // Otherwise, capitalize it + _ => capitalize_first(&p.name), + }, + short: p.name, + }) + } else { + None + } + }) .collect::>(); HttpResponse::Ok().json(platforms) } diff --git a/src/routes/v2/version_creation.rs b/src/routes/v2/version_creation.rs index 997189dc..0c7bffb4 100644 --- a/src/routes/v2/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -100,34 +100,66 @@ pub async fn version_create( json!(legacy_create.game_versions), ); + // Get all possible side-types for loaders given- we will use these to check if we need to convert/apply singleplayer, etc. + let loaders = match v3::tags::loader_list(client.clone(), redis.clone()).await { + Ok(loader_response) => match v2_reroute::extract_ok_json::< + Vec, + >(loader_response) + .await + { + Ok(loaders) => loaders, + Err(_) => vec![], + }, + Err(_) => vec![], + }; + + let loader_fields_aggregate = loaders + .into_iter() + .filter_map(|loader| { + if legacy_create.loaders.contains(&Loader(loader.name.clone())) { + Some(loader.supported_fields) + } else { + None + } + }) + .flatten() + .collect::>(); + // Copies side types of another version of the project. // If no version exists, defaults to all false. - // TODO: write test for this to ensure predictible unchanging behaviour // This is inherently lossy, but not much can be done about it, as side types are no longer associated with projects, - // so the 'missing' ones can't be easily accessed. + // so the 'missing' ones can't be easily accessed, and versions do need to have these fields explicitly set. let side_type_loader_field_names = [ "singleplayer", "client_and_server", "client_only", "server_only", ]; - fields.extend( - side_type_loader_field_names - .iter() - .map(|f| (f.to_string(), json!(false))), - ); - if let Some(example_version_fields) = - get_example_version_fields(legacy_create.project_id, client, &redis).await? - { - fields.extend(example_version_fields.into_iter().filter_map(|f| { - if side_type_loader_field_names.contains(&f.field_name.as_str()) { - Some((f.field_name, f.value.serialize_internal())) - } else { - None - } - })); - } + // Check if loader_fields_aggregate contains any of these side types + // We assume these four fields are linked together. + if loader_fields_aggregate + .iter() + .any(|f| side_type_loader_field_names.contains(&f.as_str())) + { + // If so, we get the fields of the example version of the project, and set the side types to match. + fields.extend( + side_type_loader_field_names + .iter() + .map(|f| (f.to_string(), json!(false))), + ); + if let Some(example_version_fields) = + get_example_version_fields(legacy_create.project_id, client, &redis).await? + { + fields.extend(example_version_fields.into_iter().filter_map(|f| { + if side_type_loader_field_names.contains(&f.field_name.as_str()) { + Some((f.field_name, f.value.serialize_internal())) + } else { + None + } + })); + } + } // Handle project type via file extension prediction let mut project_type = None; for file_part in &legacy_create.file_parts { diff --git a/src/routes/v2_reroute.rs b/src/routes/v2_reroute.rs index cd83b0c5..40a0d923 100644 --- a/src/routes/v2_reroute.rs +++ b/src/routes/v2_reroute.rs @@ -300,6 +300,14 @@ pub fn convert_side_types_v2_bools( } } +pub fn capitalize_first(input: &str) -> String { + let mut result = input.to_owned(); + if let Some(first_char) = result.get_mut(0..1) { + first_char.make_ascii_uppercase(); + } + result +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/routes/v3/collections.rs b/src/routes/v3/collections.rs index 0fb4020a..7eff98c5 100644 --- a/src/routes/v3/collections.rs +++ b/src/routes/v3/collections.rs @@ -46,7 +46,7 @@ pub struct CollectionCreateData { pub name: String, #[validate(length(min = 3, max = 255))] /// A short description of the collection. - pub description: String, + pub description: Option, #[validate(length(max = 32))] #[serde(default = "Vec::new")] /// A list of initial projects to use with the created collection @@ -198,7 +198,12 @@ pub struct EditCollection { )] pub name: Option, #[validate(length(min = 3, max = 256))] - pub description: Option, + #[serde( + default, + skip_serializing_if = "Option::is_none", + with = "::serde_with::rust::double_option" + )] + pub description: Option>, pub status: Option, #[validate(length(max = 64))] pub new_projects: Option>, @@ -260,7 +265,7 @@ pub async fn collection_edit( SET description = $1 WHERE (id = $2) ", - description, + description.as_ref(), id as database::models::ids::CollectionId, ) .execute(&mut *transaction) @@ -328,11 +333,22 @@ pub async fn collection_edit( ) .execute(&mut *transaction) .await?; + + sqlx::query!( + " + UPDATE collections + SET updated = NOW() + WHERE id = $1 + ", + collection_item.id as database::models::ids::CollectionId, + ) + .execute(&mut *transaction) + .await?; } + transaction.commit().await?; database::models::Collection::clear_cache(collection_item.id, &redis).await?; - transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::NotFound) @@ -417,9 +433,8 @@ pub async fn collection_icon_edit( .execute(&mut *transaction) .await?; - database::models::Collection::clear_cache(collection_item.id, &redis).await?; - transaction.commit().await?; + database::models::Collection::clear_cache(collection_item.id, &redis).await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -481,9 +496,8 @@ pub async fn delete_collection_icon( .execute(&mut *transaction) .await?; - database::models::Collection::clear_cache(collection_item.id, &redis).await?; - transaction.commit().await?; + database::models::Collection::clear_cache(collection_item.id, &redis).await?; Ok(HttpResponse::NoContent().body("")) } @@ -519,9 +533,9 @@ pub async fn collection_delete( let result = database::models::Collection::remove(collection.id, &mut transaction, &redis).await?; - database::models::Collection::clear_cache(collection.id, &redis).await?; transaction.commit().await?; + database::models::Collection::clear_cache(collection.id, &redis).await?; if result.is_some() { Ok(HttpResponse::NoContent().body("")) diff --git a/src/routes/v3/organizations.rs b/src/routes/v3/organizations.rs index 61400bed..885c7e54 100644 --- a/src/routes/v3/organizations.rs +++ b/src/routes/v3/organizations.rs @@ -456,6 +456,7 @@ pub async fn organizations_edit( .await?; } + transaction.commit().await?; database::models::Organization::clear_cache( organization_item.id, Some(organization_item.name), @@ -463,7 +464,6 @@ pub async fn organizations_edit( ) .await?; - transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthentication( @@ -819,6 +819,7 @@ pub async fn organization_icon_edit( .execute(&mut *transaction) .await?; + transaction.commit().await?; database::models::Organization::clear_cache( organization_item.id, Some(organization_item.name), @@ -826,8 +827,6 @@ pub async fn organization_icon_edit( ) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::InvalidInput(format!( @@ -904,6 +903,8 @@ pub async fn delete_organization_icon( .execute(&mut *transaction) .await?; + transaction.commit().await?; + database::models::Organization::clear_cache( organization_item.id, Some(organization_item.name), @@ -911,7 +912,5 @@ pub async fn delete_organization_icon( ) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } diff --git a/src/routes/v3/payouts.rs b/src/routes/v3/payouts.rs index 008a8e21..981384da 100644 --- a/src/routes/v3/payouts.rs +++ b/src/routes/v3/payouts.rs @@ -144,12 +144,6 @@ pub async fn paypal_webhook( .execute(&mut *transaction) .await?; - crate::database::models::user_item::User::clear_caches( - &[(crate::database::models::UserId(result.user_id), None)], - &redis, - ) - .await?; - sqlx::query!( " UPDATE payouts @@ -168,6 +162,12 @@ pub async fn paypal_webhook( .await?; transaction.commit().await?; + + crate::database::models::user_item::User::clear_caches( + &[(crate::database::models::UserId(result.user_id), None)], + &redis, + ) + .await?; } } "PAYMENT.PAYOUTS-ITEM.SUCCEEDED" => { @@ -265,12 +265,6 @@ pub async fn tremendous_webhook( .execute(&mut *transaction) .await?; - crate::database::models::user_item::User::clear_caches( - &[(crate::database::models::UserId(result.user_id), None)], - &redis, - ) - .await?; - sqlx::query!( " UPDATE payouts @@ -289,6 +283,12 @@ pub async fn tremendous_webhook( .await?; transaction.commit().await?; + + crate::database::models::user_item::User::clear_caches( + &[(crate::database::models::UserId(result.user_id), None)], + &redis, + ) + .await?; } } "REWARDS.DELIVERY.SUCCEEDED" => { @@ -616,9 +616,9 @@ pub async fn create_payout( .execute(&mut *transaction) .await?; payout_item.insert(&mut transaction).await?; - crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; transaction.commit().await?; + crate::database::models::User::clear_caches(&[(user.id, None)], &redis).await?; Ok(HttpResponse::NoContent().finish()) } diff --git a/src/routes/v3/projects.rs b/src/routes/v3/projects.rs index c647429b..0cca8938 100644 --- a/src/routes/v3/projects.rs +++ b/src/routes/v3/projects.rs @@ -839,6 +839,8 @@ pub async fn project_edit( }; img::delete_unused_images(context, checkable_strings, &mut transaction, &redis).await?; + + transaction.commit().await?; db_models::Project::clear_cache( project_item.inner.id, project_item.inner.slug, @@ -847,7 +849,6 @@ pub async fn project_edit( ) .await?; - transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthentication( @@ -1501,6 +1502,7 @@ pub async fn project_icon_edit( .execute(&mut *transaction) .await?; + transaction.commit().await?; db_models::Project::clear_cache( project_item.inner.id, project_item.inner.slug, @@ -1509,8 +1511,6 @@ pub async fn project_icon_edit( ) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::InvalidInput(format!( @@ -1596,11 +1596,10 @@ pub async fn delete_project_icon( .execute(&mut *transaction) .await?; + transaction.commit().await?; db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } @@ -1736,6 +1735,7 @@ pub async fn add_gallery_item( }]; GalleryItem::insert_many(gallery_item, project_item.inner.id, &mut transaction).await?; + transaction.commit().await?; db_models::Project::clear_cache( project_item.inner.id, project_item.inner.slug, @@ -1744,8 +1744,6 @@ pub async fn add_gallery_item( ) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::InvalidInput(format!( @@ -1921,11 +1919,11 @@ pub async fn edit_gallery_item( .await?; } + transaction.commit().await?; + db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } @@ -2027,11 +2025,11 @@ pub async fn delete_gallery_item( .execute(&mut *transaction) .await?; + transaction.commit().await?; + db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis) .await?; - transaction.commit().await?; - Ok(HttpResponse::NoContent().body("")) } diff --git a/src/routes/v3/tags.rs b/src/routes/v3/tags.rs index 8d4cfdf6..352b3eb0 100644 --- a/src/routes/v3/tags.rs +++ b/src/routes/v3/tags.rs @@ -217,6 +217,7 @@ pub async fn license_text(params: web::Path<(String,)>) -> Result = LinkPlatform::list(&**pool, &redis) .await? .into_iter() - .map(|x| LinkPlatformQueryData { name: x.name }) + .map(|x| LinkPlatformQueryData { + name: x.name, + donation: x.donation, + }) .collect(); Ok(HttpResponse::Ok().json(results)) } diff --git a/src/routes/v3/teams.rs b/src/routes/v3/teams.rs index 81c4d601..338d97b1 100644 --- a/src/routes/v3/teams.rs +++ b/src/routes/v3/teams.rs @@ -348,10 +348,10 @@ pub async fn join_team( ) .await?; + transaction.commit().await?; + User::clear_project_cache(&[current_user.id.into()], &redis).await?; TeamMember::clear_cache(team_id, &redis).await?; - - transaction.commit().await?; } else { return Err(ApiError::InvalidInput( "There is no pending request from this team".to_string(), @@ -542,9 +542,8 @@ pub async fn add_team_member( } } - TeamMember::clear_cache(team_id, &redis).await?; - transaction.commit().await?; + TeamMember::clear_cache(team_id, &redis).await?; Ok(HttpResponse::NoContent().body("")) } @@ -691,9 +690,8 @@ pub async fn edit_team_member( ) .await?; - TeamMember::clear_cache(id, &redis).await?; - transaction.commit().await?; + TeamMember::clear_cache(id, &redis).await?; Ok(HttpResponse::NoContent().body("")) } @@ -797,9 +795,8 @@ pub async fn transfer_ownership( ) .await?; - TeamMember::clear_cache(id.into(), &redis).await?; - transaction.commit().await?; + TeamMember::clear_cache(id.into(), &redis).await?; Ok(HttpResponse::NoContent().body("")) } @@ -925,10 +922,11 @@ pub async fn remove_team_member( } } + transaction.commit().await?; + TeamMember::clear_cache(id, &redis).await?; User::clear_project_cache(&[delete_member.user_id], &redis).await?; - transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::NotFound) diff --git a/src/routes/v3/users.rs b/src/routes/v3/users.rs index 2f8ec604..f997dd70 100644 --- a/src/routes/v3/users.rs +++ b/src/routes/v3/users.rs @@ -449,8 +449,8 @@ pub async fn user_edit( .await?; } - User::clear_caches(&[(id, Some(actual_user.username))], &redis).await?; transaction.commit().await?; + User::clear_caches(&[(id, Some(actual_user.username))], &redis).await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthentication( diff --git a/src/routes/v3/versions.rs b/src/routes/v3/versions.rs index 7c27c986..9ad6495d 100644 --- a/src/routes/v3/versions.rs +++ b/src/routes/v3/versions.rs @@ -664,6 +664,7 @@ pub async fn version_edit_helper( img::delete_unused_images(context, checkable_strings, &mut transaction, &redis).await?; + transaction.commit().await?; database::models::Version::clear_cache(&version_item, &redis).await?; database::models::Project::clear_cache( version_item.inner.project_id, @@ -672,7 +673,6 @@ pub async fn version_edit_helper( &redis, ) .await?; - transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) } else { Err(ApiError::CustomAuthentication( @@ -921,8 +921,8 @@ pub async fn version_schedule( .execute(&mut *transaction) .await?; - database::models::Version::clear_cache(&version_item, &redis).await?; transaction.commit().await?; + database::models::Version::clear_cache(&version_item, &redis).await?; Ok(HttpResponse::NoContent().body("")) } else { @@ -1004,12 +1004,11 @@ pub async fn version_delete( let result = database::models::Version::remove_full(version.inner.id, &redis, &mut transaction).await?; + transaction.commit().await?; remove_documents(&[version.inner.id.into()], &search_config).await?; database::models::Project::clear_cache(version.inner.project_id, None, Some(true), &redis) .await?; - transaction.commit().await?; - if result.is_some() { Ok(HttpResponse::NoContent().body("")) } else { diff --git a/tests/common/api_common/generic.rs b/tests/common/api_common/generic.rs index 7159853a..84f0d960 100644 --- a/tests/common/api_common/generic.rs +++ b/tests/common/api_common/generic.rs @@ -80,6 +80,8 @@ delegate_api_variant!( [add_gallery_item, ServiceResponse, id_or_slug: &str, image: ImageData, featured: bool, title: Option, description: Option, ordering: Option, 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, 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>], } ); diff --git a/tests/common/api_common/mod.rs b/tests/common/api_common/mod.rs index b720125d..0fa7faf9 100644 --- a/tests/common/api_common/mod.rs +++ b/tests/common/api_common/mod.rs @@ -1,8 +1,8 @@ use std::collections::HashMap; use self::models::{ - CommonCategoryData, CommonLoaderData, CommonNotification, CommonProject, CommonTeamMember, - CommonVersion, + CommonCategoryData, CommonItemType, CommonLoaderData, CommonNotification, CommonProject, + CommonTeamMember, CommonVersion, }; use self::request_data::{ImageData, ProjectCreationRequestData}; use actix_web::dev::ServiceResponse; @@ -118,6 +118,15 @@ pub trait ApiProject { patch: HashMap, 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_trait(?Send)] diff --git a/tests/common/api_common/models.rs b/tests/common/api_common/models.rs index 994a7b5d..e8776f2c 100644 --- a/tests/common/api_common/models.rs +++ b/tests/common/api_common/models.rs @@ -11,7 +11,7 @@ use labrinth::models::{ users::{User, UserId}, }; use rust_decimal::Decimal; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; // Fields shared by every version of the API. // No struct in here should have ANY field that @@ -119,3 +119,23 @@ pub struct CommonNotification { pub struct CommonNotificationAction { pub action_route: (String, String), } + +#[derive(Serialize, 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", + } + } +} diff --git a/tests/common/api_v2/project.rs b/tests/common/api_v2/project.rs index 591ebe62..56fe12b3 100644 --- a/tests/common/api_v2/project.rs +++ b/tests/common/api_v2/project.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use crate::common::{ api_common::{ - models::{CommonProject, CommonVersion}, + models::{CommonItemType, CommonProject, CommonVersion}, request_data::{ImageData, ProjectCreationRequestData}, Api, ApiProject, AppendsOptionalPat, }, @@ -266,6 +266,39 @@ impl ApiProject for ApiV2 { } } + 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}", id = id)) + .append_pat(pat) + .to_request(); + + self.call(req).await + } + async fn schedule_project( &self, id_or_slug: &str, diff --git a/tests/common/api_v2/tags.rs b/tests/common/api_v2/tags.rs index 62720fde..08fb4b88 100644 --- a/tests/common/api_v2/tags.rs +++ b/tests/common/api_v2/tags.rs @@ -3,7 +3,9 @@ use actix_web::{ test::{self, TestRequest}, }; use async_trait::async_trait; -use labrinth::routes::v2::tags::{CategoryData, GameVersionQueryData, LoaderData}; +use labrinth::routes::v2::tags::{ + CategoryData, DonationPlatformQueryData, GameVersionQueryData, LoaderData, +}; use crate::common::{ api_common::{ @@ -57,6 +59,21 @@ impl ApiV2 { assert_eq!(resp.status(), 200); 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 { + let resp = self.get_donation_platforms().await; + println!("Response: {:?}", resp.response().body()); + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } } #[async_trait(?Send)] diff --git a/tests/common/api_v3/project.rs b/tests/common/api_v3/project.rs index f206480e..2c678f04 100644 --- a/tests/common/api_v3/project.rs +++ b/tests/common/api_v3/project.rs @@ -17,7 +17,7 @@ use serde_json::json; use crate::common::{ api_common::{ - models::{CommonProject, CommonVersion}, + models::{CommonItemType, CommonProject, CommonVersion}, request_data::{ImageData, ProjectCreationRequestData}, Api, ApiProject, AppendsOptionalPat, }, @@ -219,6 +219,39 @@ impl ApiProject for ApiV3 { } } + 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}", id = id)) + .append_pat(pat) + .to_request(); + + self.call(req).await + } + async fn schedule_project( &self, id_or_slug: &str, diff --git a/tests/project.rs b/tests/project.rs index 2a69280f..c57af867 100644 --- a/tests/project.rs +++ b/tests/project.rs @@ -18,6 +18,7 @@ use labrinth::models::teams::ProjectPermissions; use labrinth::util::actix::{MultipartSegment, MultipartSegmentData}; use serde_json::json; +use crate::common::api_common::models::CommonItemType; use crate::common::api_common::request_data::ProjectCreationRequestData; use crate::common::api_common::{ApiProject, ApiTeams, ApiVersion, AppendsOptionalPat}; use crate::common::dummy_data::{DummyImage, TestFile}; @@ -590,6 +591,94 @@ pub async fn test_bulk_edit_links() { .await; } +#[actix_rt::test] +async fn delete_project_with_report() { + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + let alpha_project_id: &str = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let beta_project_id: &str = &test_env.dummy.as_ref().unwrap().project_beta.project_id; + + // Create a report for the project + let resp = api + .create_report( + "copyright", + alpha_project_id, + CommonItemType::Project, + "Hey! This is my project, copied without permission!", + ENEMY_USER_PAT, // Enemy makes a report + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let value = test::read_body_json::(resp).await; + let alpha_report_id = value["id"].as_str().unwrap(); + + // Confirm existence + let resp = api + .get_report( + alpha_report_id, + ENEMY_USER_PAT, // Enemy makes a report + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + + // Do the same for beta + let resp = api + .create_report( + "copyright", + beta_project_id, + CommonItemType::Project, + "Hey! This is my project, copied without permission!", + ENEMY_USER_PAT, // Enemy makes a report + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let value = test::read_body_json::(resp).await; + let beta_report_id = value["id"].as_str().unwrap(); + + // Delete the project + let resp = api.remove_project(alpha_project_id, USER_USER_PAT).await; + assert_eq!(resp.status(), StatusCode::NO_CONTENT); + + // Confirm that the project is gone from the cache + let mut redis_pool = test_env.db.redis_pool.connect().await.unwrap(); + assert_eq!( + redis_pool + .get(PROJECTS_SLUGS_NAMESPACE, "demo") + .await + .unwrap() + .and_then(|x| x.parse::().ok()), + None + ); + assert_eq!( + redis_pool + .get(PROJECTS_SLUGS_NAMESPACE, alpha_project_id) + .await + .unwrap() + .and_then(|x| x.parse::().ok()), + None + ); + + // Report for alpha no longer exists + let resp = api + .get_report( + alpha_report_id, + ENEMY_USER_PAT, // Enemy makes a report + ) + .await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + // Confirm that report for beta still exists + let resp = api + .get_report( + beta_report_id, + ENEMY_USER_PAT, // Enemy makes a report + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + }) + .await; +} + #[actix_rt::test] async fn permissions_patch_project_v3() { with_test_environment(Some(8), |test_env: TestEnvironment| async move { diff --git a/tests/v2/tags.rs b/tests/v2/tags.rs index cb072ba0..9b854337 100644 --- a/tests/v2/tags.rs +++ b/tests/v2/tags.rs @@ -1,4 +1,5 @@ use itertools::Itertools; +use labrinth::routes::v2::tags::DonationPlatformQueryData; use std::collections::HashSet; @@ -62,3 +63,45 @@ async fn get_tags() { }) .await; } + +#[actix_rt::test] +async fn get_donation_platforms() { + with_test_environment(None, |test_env: TestEnvironment| 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; +}