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>
This commit is contained in:
Wyatt Verchere
2023-12-14 15:19:50 -07:00
committed by GitHub
parent 50e89ad98b
commit f939e59463
33 changed files with 494 additions and 112 deletions

View File

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

View File

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

View File

@@ -62,7 +62,7 @@
"nullable": [
false,
false,
false,
true,
true,
true,
false,

View File

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

View File

@@ -0,0 +1 @@
ALTER TABLE collections ALTER COLUMN description DROP NOT NULL;

View File

@@ -13,7 +13,7 @@ pub struct CollectionBuilder {
pub collection_id: CollectionId,
pub user_id: UserId,
pub name: String,
pub description: String,
pub description: Option<String>,
pub status: CollectionStatus,
pub projects: Vec<ProjectId>,
}
@@ -45,7 +45,7 @@ pub struct Collection {
pub id: CollectionId,
pub user_id: UserId,
pub name: String,
pub description: String,
pub description: Option<String>,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub icon_url: Option<String>,
@@ -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(),

View File

@@ -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::<Vec<_>>()
.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

View File

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

View File

@@ -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<String>,
/// An icon URL for the collection.
pub icon_url: Option<String>,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<HttpResponse,
.or_else(v2_reroute::flatten_404_error)
}
#[derive(serde::Serialize)]
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Eq, Debug)]
pub struct DonationPlatformQueryData {
// The difference between name and short is removed in v3.
// Now, the 'id' becomes the name, and the 'name' is removed (the frontend uses the id as the name)
// pub short: String,
pub short: String,
pub name: String,
}
@@ -193,7 +195,26 @@ pub async fn donation_platform_list(
Ok(platforms) => {
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::<Vec<_>>();
HttpResponse::Ok().json(platforms)
}

View File

@@ -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<v3::tags::LoaderData>,
>(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::<Vec<_>>();
// 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 {

View File

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

View File

@@ -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<String>,
#[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<String>,
#[validate(length(min = 3, max = 256))]
pub description: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
pub description: Option<Option<String>>,
pub status: Option<CollectionStatus>,
#[validate(length(max = 64))]
pub new_projects: Option<Vec<String>>,
@@ -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(""))

View File

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

View File

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

View File

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

View File

@@ -217,6 +217,7 @@ pub async fn license_text(params: web::Path<(String,)>) -> Result<HttpResponse,
#[derive(serde::Serialize, serde::Deserialize)]
pub struct LinkPlatformQueryData {
pub name: String,
pub donation: bool,
}
pub async fn link_platform_list(
@@ -226,7 +227,10 @@ pub async fn link_platform_list(
let results: Vec<LinkPlatformQueryData> = 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))
}

View File

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

View File

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

View File

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

View File

@@ -80,6 +80,8 @@ delegate_api_variant!(
[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>],
}
);

View File

@@ -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<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_trait(?Send)]

View File

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

View File

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

View File

@@ -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<DonationPlatformQueryData> {
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)]

View File

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

View File

@@ -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<ApiV3>| 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::<serde_json::Value, _>(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::<serde_json::Value, _>(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::<i64>().ok()),
None
);
assert_eq!(
redis_pool
.get(PROJECTS_SLUGS_NAMESPACE, alpha_project_id)
.await
.unwrap()
.and_then(|x| x.parse::<i64>().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<ApiV3>| async move {

View File

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