Alejandro González f84f8c1c2b
chore(clippy): enable and fix many stricter lints (#3783)
* chore(clippy): enable and fix many stricter lints

These ensure that the codebase uses more idiomatic, performant, and
concise language constructions.

* chore: make non-Clippy compiler warnings also deny by default
2025-06-14 00:10:12 +00:00

1365 lines
50 KiB
Rust

use actix_http::StatusCode;
use actix_web::test;
use common::api_v3::ApiV3;
use common::database::*;
use common::dummy_data::DUMMY_CATEGORIES;
use crate::common::api_common::models::CommonProject;
use crate::common::api_common::request_data::ProjectCreationRequestData;
use crate::common::api_common::{ApiProject, ApiTeams, ApiVersion};
use crate::common::dummy_data::{
DummyImage, DummyOrganizationZeta, DummyProjectAlpha, DummyProjectBeta,
TestFile,
};
use ariadne::ids::base62_impl::parse_base62;
use common::environment::{
TestEnvironment, with_test_environment, with_test_environment_all,
};
use common::permissions::{PermissionsTest, PermissionsTestContext};
use futures::StreamExt;
use hex::ToHex;
use labrinth::database::models::project_item::{
PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE,
};
use labrinth::models::ids::ProjectId;
use labrinth::models::teams::ProjectPermissions;
use labrinth::util::actix::{MultipartSegment, MultipartSegmentData};
use serde_json::json;
use sha1::Digest;
pub mod common;
#[actix_rt::test]
async fn test_get_project() {
// Test setup and dummy data
with_test_environment_all(None, |test_env| async move {
let DummyProjectAlpha {
project_id: alpha_project_id,
project_slug: alpha_project_slug,
version_id: alpha_version_id,
..
} = &test_env.dummy.project_alpha;
let DummyProjectBeta {
project_id: beta_project_id,
..
} = &test_env.dummy.project_beta;
let api = &test_env.api;
// Perform request on dummy data
let resp = api.get_project(alpha_project_id, USER_USER_PAT).await;
assert_status!(&resp, StatusCode::OK);
let body: serde_json::Value = test::read_body_json(resp).await;
assert_eq!(body["id"], json!(alpha_project_id));
assert_eq!(body["slug"], json!(alpha_project_slug));
let versions = body["versions"].as_array().unwrap();
assert_eq!(versions[0], json!(alpha_version_id));
// Confirm that the request was cached
let mut redis_pool = test_env.db.redis_pool.connect().await.unwrap();
assert_eq!(
redis_pool
.get(PROJECTS_SLUGS_NAMESPACE, alpha_project_slug)
.await
.unwrap()
.and_then(|x| x.parse::<i64>().ok()),
Some(parse_base62(alpha_project_id).unwrap() as i64)
);
let cached_project = redis_pool
.get(
PROJECTS_NAMESPACE,
&parse_base62(alpha_project_id).unwrap().to_string(),
)
.await
.unwrap()
.unwrap();
let cached_project: serde_json::Value =
serde_json::from_str(&cached_project).unwrap();
assert_eq!(
cached_project["val"]["inner"]["slug"],
json!(alpha_project_slug)
);
// Make the request again, this time it should be cached
let resp = api.get_project(alpha_project_id, USER_USER_PAT).await;
assert_status!(&resp, StatusCode::OK);
let body: serde_json::Value = test::read_body_json(resp).await;
assert_eq!(body["id"], json!(alpha_project_id));
assert_eq!(body["slug"], json!(alpha_project_slug));
// Request should fail on non-existent project
let resp = api.get_project("nonexistent", 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)
let resp = api.get_project(beta_project_id, ENEMY_USER_PAT).await;
assert_status!(&resp, StatusCode::NOT_FOUND);
})
.await;
}
#[actix_rt::test]
async fn test_add_remove_project() {
// Test setup and dummy data
with_test_environment(
None,
|test_env: TestEnvironment<ApiV3>| async move {
let api = &test_env.api;
// Generate test project data.
let mut json_data = api
.get_public_project_creation_data_json(
"demo",
Some(&TestFile::BasicMod),
)
.await;
// 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 resp = api
.create_project(
ProjectCreationRequestData {
slug: "demo".to_string(),
segment_data: vec![
json_segment.clone(),
file_segment.clone(),
],
jar: None, // File not needed at this point
},
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::OK);
// Get the project we just made, and confirm that it's correct
let project = api
.get_project_deserialized_common("demo", USER_USER_PAT)
.await;
assert!(project.versions.len() == 1);
let uploaded_version_id = project.versions[0];
// Checks files to ensure they were uploaded and correctly identify the file
let hash = sha1::Sha1::digest(basic_mod_file.bytes())
.encode_hex::<String>();
let version = api
.get_version_from_hash_deserialized_common(
&hash,
"sha1",
USER_USER_PAT,
)
.await;
assert_eq!(version.id, uploaded_version_id);
// Reusing with a different slug and the same file should fail
// Even if that file is named differently
let resp = api
.create_project(
ProjectCreationRequestData {
slug: "demo".to_string(),
segment_data: vec![
json_diff_slug_file_segment.clone(),
file_diff_name_segment.clone(),
],
jar: None, // File not needed at this point
},
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::BAD_REQUEST);
// Reusing with the same slug and a different file should fail
let resp = api
.create_project(
ProjectCreationRequestData {
slug: "demo".to_string(),
segment_data: vec![
json_diff_file_segment.clone(),
file_diff_name_content_segment.clone(),
],
jar: None, // File not needed at this point
},
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::BAD_REQUEST);
// Different slug, different file should succeed
let resp = api
.create_project(
ProjectCreationRequestData {
slug: "demo".to_string(),
segment_data: vec![
json_diff_slug_file_segment.clone(),
file_diff_name_content_segment.clone(),
],
jar: None, // File not needed at this point
},
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::OK);
// Get
let project = api
.get_project_deserialized_common("demo", USER_USER_PAT)
.await;
let id = project.id.to_string();
// Remove the project
let resp = test_env.api.remove_project("demo", USER_USER_PAT).await;
assert_status!(&resp, 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, &id)
.await
.unwrap()
.and_then(|x| x.parse::<i64>().ok()),
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]
pub async fn test_patch_project() {
with_test_environment(
None,
|test_env: TestEnvironment<ApiV3>| async move {
let api = &test_env.api;
let alpha_project_slug = &test_env.dummy.project_alpha.project_slug;
let beta_project_slug = &test_env.dummy.project_beta.project_slug;
// First, we do some patch requests that should fail.
// Failure because the user is not authorized.
let resp = api
.edit_project(
alpha_project_slug,
json!({
"name": "Test_Add_Project project - test 1",
}),
ENEMY_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::UNAUTHORIZED);
// Failure because we are setting URL fields to invalid urls.
for url_type in ["issues", "source", "wiki", "discord"] {
let resp = api
.edit_project(
alpha_project_slug,
json!({
"link_urls": {
url_type: "not a url",
},
}),
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::BAD_REQUEST);
}
// Failure because these are illegal requested statuses for a normal user.
for req in ["unknown", "processing", "withheld", "scheduled"] {
let resp = api
.edit_project(
alpha_project_slug,
json!({
"requested_status": req,
}),
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::BAD_REQUEST);
}
// Failure because these should not be able to be set by a non-mod
for key in ["moderation_message", "moderation_message_body"] {
let resp = api
.edit_project(
alpha_project_slug,
json!({
key: "test",
}),
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::UNAUTHORIZED);
// (should work for a mod, though)
let resp = api
.edit_project(
alpha_project_slug,
json!({
key: "test",
}),
MOD_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::NO_CONTENT);
}
// Failed patch to alpha slug:
// - slug collision with beta
// - too short slug
// - too long slug
// - not url safe slug
// - not url safe slug
for slug in [
beta_project_slug,
"a",
&"a".repeat(100),
"not url safe%&^!#$##!@#$%^&*()",
] {
let resp = api
.edit_project(
alpha_project_slug,
json!({
"slug": slug, // the other dummy project has this slug
}),
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::BAD_REQUEST);
}
// Not allowed to directly set status, as 'beta_project_slug' (the other project) is "processing" and cannot have its status changed like this.
let resp = api
.edit_project(
beta_project_slug,
json!({
"status": "private"
}),
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::UNAUTHORIZED);
// Sucessful request to patch many fields.
let resp = api
.edit_project(
alpha_project_slug,
json!({
"slug": "newslug",
"categories": [DUMMY_CATEGORIES[0]],
"license_id": "MIT",
"link_urls":
{
"patreon": "https://patreon.com",
"issues": "https://github.com",
"discord": "https://discord.gg",
"wiki": "https://wiki.com"
}
}),
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::NO_CONTENT);
// Old slug no longer works
let resp = api.get_project(alpha_project_slug, USER_USER_PAT).await;
assert_status!(&resp, StatusCode::NOT_FOUND);
// New slug does work
let project =
api.get_project_deserialized("newslug", USER_USER_PAT).await;
assert_eq!(project.slug.unwrap(), "newslug");
assert_eq!(project.categories, vec![DUMMY_CATEGORIES[0]]);
assert_eq!(project.license.id, "MIT");
let link_urls = project.link_urls;
assert_eq!(link_urls.len(), 4);
assert_eq!(link_urls["patreon"].platform, "patreon");
assert_eq!(link_urls["patreon"].url, "https://patreon.com");
assert!(link_urls["patreon"].donation);
assert_eq!(link_urls["issues"].platform, "issues");
assert_eq!(link_urls["issues"].url, "https://github.com");
assert!(!link_urls["issues"].donation);
assert_eq!(link_urls["discord"].platform, "discord");
assert_eq!(link_urls["discord"].url, "https://discord.gg");
assert!(!link_urls["discord"].donation);
assert_eq!(link_urls["wiki"].platform, "wiki");
assert_eq!(link_urls["wiki"].url, "https://wiki.com");
assert!(!link_urls["wiki"].donation);
// Unset the set link_urls
let resp = api
.edit_project(
"newslug",
json!({
"link_urls":
{
"issues": null,
}
}),
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::NO_CONTENT);
let project =
api.get_project_deserialized("newslug", USER_USER_PAT).await;
assert_eq!(project.link_urls.len(), 3);
assert!(!project.link_urls.contains_key("issues"));
},
)
.await;
}
#[actix_rt::test]
pub async fn test_patch_v3() {
// Hits V3-specific patchable fields
with_test_environment(
None,
|test_env: TestEnvironment<ApiV3>| 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!({
"name": "New successful title",
"summary": "New successful summary",
"description": "New successful description",
}),
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::NO_CONTENT);
let project = api
.get_project_deserialized(alpha_project_slug, USER_USER_PAT)
.await;
assert_eq!(project.name, "New successful title");
assert_eq!(project.summary, "New successful summary");
assert_eq!(project.description, "New successful description");
},
)
.await;
}
#[actix_rt::test]
pub async fn test_bulk_edit_categories() {
with_test_environment_all(None, |test_env| 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!({
"categories": [DUMMY_CATEGORIES[0], DUMMY_CATEGORIES[3]],
"add_categories": [DUMMY_CATEGORIES[1], DUMMY_CATEGORIES[2]],
"remove_categories": [DUMMY_CATEGORIES[3]],
"additional_categories": [DUMMY_CATEGORIES[4], DUMMY_CATEGORIES[6]],
"add_additional_categories": [DUMMY_CATEGORIES[5]],
"remove_additional_categories": [DUMMY_CATEGORIES[6]],
}),
ADMIN_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::NO_CONTENT);
let alpha_body = api
.get_project_deserialized_common(alpha_project_id, ADMIN_USER_PAT)
.await;
assert_eq!(alpha_body.categories, DUMMY_CATEGORIES[0..=2]);
assert_eq!(alpha_body.additional_categories, DUMMY_CATEGORIES[4..=5]);
let beta_body = api
.get_project_deserialized_common(beta_project_id, ADMIN_USER_PAT)
.await;
assert_eq!(beta_body.categories, alpha_body.categories);
assert_eq!(
beta_body.additional_categories,
alpha_body.additional_categories,
);
})
.await;
}
#[actix_rt::test]
pub async fn test_bulk_edit_links() {
with_test_environment(
None,
|test_env: TestEnvironment<ApiV3>| 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;
// Sets links for issue, source, wiki, and patreon for all projects
// The first loop, sets issue, the second, clears it for all projects.
for issues in [Some("https://www.issues.com"), None] {
let resp = api
.edit_project_bulk(
&[alpha_project_id, beta_project_id],
json!({
"link_urls": {
"issues": issues,
"wiki": "https://wiki.com",
"patreon": "https://patreon.com",
},
}),
ADMIN_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::NO_CONTENT);
let alpha_body = api
.get_project_deserialized(alpha_project_id, ADMIN_USER_PAT)
.await;
if let Some(issues) = issues {
assert_eq!(alpha_body.link_urls.len(), 3);
assert_eq!(alpha_body.link_urls["issues"].url, issues);
} else {
assert_eq!(alpha_body.link_urls.len(), 2);
assert!(!alpha_body.link_urls.contains_key("issues"));
}
assert_eq!(
alpha_body.link_urls["wiki"].url,
"https://wiki.com"
);
assert_eq!(
alpha_body.link_urls["patreon"].url,
"https://patreon.com"
);
let beta_body = api
.get_project_deserialized(beta_project_id, ADMIN_USER_PAT)
.await;
assert_eq!(beta_body.categories, alpha_body.categories);
assert_eq!(
beta_body.additional_categories,
alpha_body.additional_categories,
);
}
},
)
.await;
}
#[actix_rt::test]
async fn permissions_patch_project_v3() {
with_test_environment(Some(8), |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 api = &test_env.api;
// TODO: This should be a separate test from v3
// - only a couple of these fields are v3-specific
// once we have permissions/scope tests setup to not just take closures, we can split this up
// For each permission covered by EDIT_DETAILS, ensure the permission is required
let edit_details = ProjectPermissions::EDIT_DETAILS;
let test_pairs = [
// Body, status, requested_status tested separately
("slug", json!("")), // generated in the test to not collide slugs
("name", json!("randomname")),
("description", json!("randomdescription")),
("categories", json!(["combat", "economy"])),
("additional_categories", json!(["decoration"])),
(
"links",
json!({
"issues": "https://issues.com",
"source": "https://source.com",
}),
),
("license_id", json!("MIT")),
];
futures::stream::iter(test_pairs)
.map(|(key, value)| {
let test_env = test_env.clone();
async move {
let req_gen = |ctx: PermissionsTestContext| {
let value = value.clone();
async move {
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;
// Test with status and requested_status
// This requires a project with a version, so we use alpha_project_id
let req_gen = |ctx: PermissionsTestContext| async move {
api.edit_project(
&ctx.project_id.unwrap(),
json!({
"status": "private",
"requested_status": "private",
}),
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(edit_details, req_gen)
.await
.unwrap();
// Bulk patch projects
let req_gen = |ctx: PermissionsTestContext| async move {
api.edit_project_bulk(
&[&ctx.project_id.unwrap()],
json!({
"name": "randomname",
}),
ctx.test_pat.as_deref(),
)
.await
};
PermissionsTest::new(&test_env)
.simple_project_permissions_test(edit_details, req_gen)
.await
.unwrap();
// Edit body
// Cannot bulk edit body
let edit_body = ProjectPermissions::EDIT_BODY;
let req_gen = |ctx: PermissionsTestContext| async move {
api.edit_project(
&ctx.project_id.unwrap(),
json!({
"description": "new description!",
}),
ctx.test_pat.as_deref(),
)
.await
};
PermissionsTest::new(&test_env)
.simple_project_permissions_test(edit_body, req_gen)
.await
.unwrap();
})
.await;
}
// TODO: Project scheduling has been temporarily disabled, so this test is disabled as well
// #[actix_rt::test]
// async fn permissions_schedule() {
// with_test_environment(None, |test_env : TestEnvironment<ApiV3>| async move {
// let DummyProjectAlpha {
// project_id: alpha_project_id,
// team_id: alpha_team_id,
// ..
// } = &test_env.dummy.project_alpha;
// let DummyProjectBeta {
// project_id: beta_project_id,
// version_id: beta_version_id,
// team_id: beta_team_id,
// ..
// } = &test_env.dummy.project_beta;
// let edit_details = ProjectPermissions::EDIT_DETAILS;
// let api = &test_env.api;
// // Approve beta version as private so we can schedule it
// let resp = api
// .edit_version(
// beta_version_id,
// json!({
// "status": "unlisted"
// }),
// MOD_USER_PAT,
// )
// .await;
// assert_status!(&resp, StatusCode::NO_CONTENT);
// // Schedule version
// let req_gen = |ctx: PermissionsTestContext| async move {
// api.schedule_version(
// beta_version_id,
// "archived",
// Utc::now() + Duration::days(1),
// ctx.test_pat.as_deref(),
// )
// .await
// };
// PermissionsTest::new(&test_env)
// .with_existing_project(beta_project_id, beta_team_id)
// .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true)
// .simple_project_permissions_test(edit_details, req_gen)
// .await
// .unwrap();
// }).await
// }
// Not covered by PATCH /project
#[actix_rt::test]
async fn permissions_edit_details() {
with_test_environment_all(Some(10), |test_env| async move {
let DummyProjectAlpha {
project_id: alpha_project_id,
team_id: alpha_team_id,
..
} = &test_env.dummy.project_alpha;
let edit_details = ProjectPermissions::EDIT_DETAILS;
let api = &test_env.api;
// Icon edit
// Uses alpha project to delete this icon
let req_gen = |ctx: PermissionsTestContext| async move {
api.edit_project_icon(
&ctx.project_id.unwrap(),
Some(DummyImage::SmallIcon.get_icon_data()),
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(edit_details, req_gen)
.await
.unwrap();
// Icon delete
// Uses alpha project to delete added icon
let req_gen = |ctx: PermissionsTestContext| async move {
api.edit_project_icon(
&ctx.project_id.unwrap(),
None,
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(edit_details, req_gen)
.await
.unwrap();
// Add gallery item
// Uses alpha project to add gallery item so we can get its url
let req_gen = |ctx: PermissionsTestContext| async move {
api.add_gallery_item(
&ctx.project_id.unwrap(),
DummyImage::SmallIcon.get_icon_data(),
true,
None,
None,
None,
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(edit_details, req_gen)
.await
.unwrap();
// Get project, as we need the gallery image url
let resp = api.get_project(alpha_project_id, USER_USER_PAT).await;
let project: serde_json::Value = test::read_body_json(resp).await;
let gallery_url = project["gallery"][0]["url"].as_str().unwrap();
// Edit gallery item
// Uses alpha project to edit gallery item
let req_gen = |ctx: PermissionsTestContext| async move {
api.edit_gallery_item(
&ctx.project_id.unwrap(),
gallery_url,
vec![("description".to_string(), "new caption!".to_string())]
.into_iter()
.collect(),
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(edit_details, req_gen)
.await
.unwrap();
// Remove gallery item
// Uses alpha project to remove gallery item
let req_gen = |ctx: PermissionsTestContext| async move {
api.remove_gallery_item(
&ctx.project_id.unwrap(),
gallery_url,
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(edit_details, req_gen)
.await
.unwrap();
})
.await;
}
#[actix_rt::test]
async fn permissions_upload_version() {
with_test_environment(
None,
|test_env: TestEnvironment<ApiV3>| 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 upload_version = ProjectPermissions::UPLOAD_VERSION;
// Upload version with basic-mod.jar
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 req_gen = |ctx: PermissionsTestContext| async move {
api.upload_file_to_version(
alpha_version_id,
&TestFile::BasicModDifferent,
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]
async fn permissions_manage_invites() {
// Add member, remove member, edit member
with_test_environment_all(None, |test_env| 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 api = &test_env.api;
let manage_invites = ProjectPermissions::MANAGE_INVITES;
// Add member
let req_gen = |ctx: PermissionsTestContext| async move {
api.add_user_to_team(
&ctx.team_id.unwrap(),
MOD_USER_ID,
Some(ProjectPermissions::empty()),
None,
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(manage_invites, req_gen)
.await
.unwrap();
// Edit member
let edit_member = ProjectPermissions::EDIT_MEMBER;
let req_gen = |ctx: PermissionsTestContext| async move {
api.edit_team_member(
&ctx.team_id.unwrap(),
MOD_USER_ID,
json!({
"permissions": 0,
}),
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(edit_member, req_gen)
.await
.unwrap();
// remove member
// requires manage_invites if they have not yet accepted the invite
let req_gen = |ctx: PermissionsTestContext| async move {
api.remove_from_team(
&ctx.team_id.unwrap(),
MOD_USER_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(manage_invites, req_gen)
.await
.unwrap();
// re-add member for testing
let resp = api
.add_user_to_team(
alpha_team_id,
MOD_USER_ID,
Some(ProjectPermissions::empty()),
None,
ADMIN_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::NO_CONTENT);
// Accept invite
let resp = api.join_team(alpha_team_id, MOD_USER_PAT).await;
assert_status!(&resp, StatusCode::NO_CONTENT);
// remove existing member (requires remove_member)
let remove_member = ProjectPermissions::REMOVE_MEMBER;
let req_gen = |ctx: PermissionsTestContext| async move {
api.remove_from_team(
&ctx.team_id.unwrap(),
MOD_USER_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(remove_member, req_gen)
.await
.unwrap();
})
.await;
}
#[actix_rt::test]
async fn permissions_delete_project() {
// Add member, remove member, edit member
with_test_environment_all(None, |test_env| async move {
let delete_project = ProjectPermissions::DELETE_PROJECT;
let api = &test_env.api;
// Delete project
let req_gen = |ctx: PermissionsTestContext| async move {
api.remove_project(
&ctx.project_id.unwrap(),
ctx.test_pat.as_deref(),
)
.await
};
PermissionsTest::new(&test_env)
.simple_project_permissions_test(delete_project, req_gen)
.await
.unwrap();
test_env.cleanup().await;
})
.await;
}
#[actix_rt::test]
async fn project_permissions_consistency_test() {
with_test_environment_all(Some(10), |test_env| async move {
// Test that the permissions are consistent with each other
// For example, if we get the projectpermissions directly, from an organization's defaults, overriden, etc, they should all be correct & consistent
let api = &test_env.api;
// Full project permissions test with EDIT_DETAILS
let success_permissions = ProjectPermissions::EDIT_DETAILS;
let req_gen = |ctx: PermissionsTestContext| async move {
api.edit_project(
&ctx.project_id.unwrap(),
json!({
"categories": [],
}),
ctx.test_pat.as_deref(),
)
.await
};
PermissionsTest::new(&test_env)
.full_project_permissions_test(success_permissions, req_gen)
.await
.unwrap();
// We do a test with more specific permissions, to ensure that *exactly* the permissions at each step are as expected
let success_permissions = ProjectPermissions::EDIT_DETAILS
| ProjectPermissions::REMOVE_MEMBER
| ProjectPermissions::DELETE_VERSION
| ProjectPermissions::VIEW_PAYOUTS;
let req_gen = |ctx: PermissionsTestContext| async move {
api.edit_project(
&ctx.project_id.unwrap(),
json!({
"categories": [],
}),
ctx.test_pat.as_deref(),
)
.await
};
PermissionsTest::new(&test_env)
.full_project_permissions_test(success_permissions, req_gen)
.await
.unwrap();
})
.await;
}
// TODO: Re-add this if we want to match v3 Projects structure to v3 Search Result structure, otherwise, delete
// #[actix_rt::test]
// async fn align_search_projects() {
// // Test setup and dummy data
// with_test_environment(Some(10), |test_env: TestEnvironment<ApiV3>| async move {
// setup_search_projects(&test_env).await;
// let api = &test_env.api;
// let test_name = test_env.db.database_name.clone();
// let projects = api
// .search_deserialized(
// Some(&format!("\"&{test_name}\"")),
// Some(json!([["categories:fabric"]])),
// USER_USER_PAT,
// )
// .await;
// for project in projects.hits {
// let project_model = api
// .get_project(&project.id.to_string(), USER_USER_PAT)
// .await;
// assert_status!(&project_model, StatusCode::OK);
// let mut project_model: Project = test::read_body_json(project_model).await;
// // Body/description is huge- don't store it in search, so it's StatusCode::OK if they differ here
// // (Search should return "")
// project_model.description = "".into();
// let project_model = serde_json::to_value(project_model).unwrap();
// let searched_project_serialized = serde_json::to_value(project).unwrap();
// assert_eq!(project_model, searched_project_serialized);
// }
// })
// .await
// }
#[actix_rt::test]
async fn projects_various_visibility() {
// For testing the filter_visible_projects and is_visible_project
with_test_environment(
None,
|env: common::environment::TestEnvironment<ApiV3>| async move {
let DummyProjectAlpha {
project_id: alpha_project_id,
project_id_parsed: alpha_project_id_parsed,
..
} = &env.dummy.project_alpha;
let DummyProjectBeta {
project_id: beta_project_id,
project_id_parsed: beta_project_id_parsed,
..
} = &env.dummy.project_beta;
let DummyOrganizationZeta {
organization_id: zeta_organization_id,
team_id: zeta_team_id,
..
} = &env.dummy.organization_zeta;
// Invite friend to org zeta and accept it
let resp = env
.api
.add_user_to_team(
zeta_team_id,
FRIEND_USER_ID,
Some(ProjectPermissions::empty()),
None,
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::NO_CONTENT);
let resp = env.api.join_team(zeta_team_id, FRIEND_USER_PAT).await;
assert_status!(&resp, StatusCode::NO_CONTENT);
let visible_pat_pairs = vec![
(&alpha_project_id_parsed, USER_USER_PAT, StatusCode::OK),
(&alpha_project_id_parsed, FRIEND_USER_PAT, StatusCode::OK),
(&alpha_project_id_parsed, ENEMY_USER_PAT, StatusCode::OK),
(&beta_project_id_parsed, USER_USER_PAT, StatusCode::OK),
(
&beta_project_id_parsed,
FRIEND_USER_PAT,
StatusCode::NOT_FOUND,
),
(
&beta_project_id_parsed,
ENEMY_USER_PAT,
StatusCode::NOT_FOUND,
),
];
// Tests get_project, a route that uses is_visible_project
for (project_id, pat, expected_status) in visible_pat_pairs {
let resp =
env.api.get_project(&project_id.to_string(), pat).await;
assert_status!(&resp, expected_status);
}
// Test get_user_projects, a route that uses filter_visible_projects
let visible_pat_pairs = vec![
(USER_USER_PAT, 2),
(FRIEND_USER_PAT, 1),
(ENEMY_USER_PAT, 1),
];
for (pat, expected_count) in visible_pat_pairs {
let projects = env
.api
.get_user_projects_deserialized_common(USER_USER_ID, pat)
.await;
assert_eq!(projects.len(), expected_count);
}
// Add projects to org zeta
let resp = env
.api
.organization_add_project(
zeta_organization_id,
alpha_project_id,
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::OK);
let resp = env
.api
.organization_add_project(
zeta_organization_id,
beta_project_id,
USER_USER_PAT,
)
.await;
assert_status!(&resp, StatusCode::OK);
// Test get_project, a route that uses is_visible_project
let visible_pat_pairs = vec![
(&alpha_project_id_parsed, USER_USER_PAT, StatusCode::OK),
(&alpha_project_id_parsed, FRIEND_USER_PAT, StatusCode::OK),
(&alpha_project_id_parsed, ENEMY_USER_PAT, StatusCode::OK),
(&beta_project_id_parsed, USER_USER_PAT, StatusCode::OK),
(&beta_project_id_parsed, FRIEND_USER_PAT, StatusCode::OK),
(
&beta_project_id_parsed,
ENEMY_USER_PAT,
StatusCode::NOT_FOUND,
),
];
for (project_id, pat, expected_status) in visible_pat_pairs {
let resp =
env.api.get_project(&project_id.to_string(), pat).await;
assert_status!(&resp, expected_status);
}
// Test get_user_projects, a route that uses filter_visible_projects
let visible_pat_pairs = vec![
(USER_USER_PAT, 2),
(FRIEND_USER_PAT, 2),
(ENEMY_USER_PAT, 1),
];
for (pat, expected_count) in visible_pat_pairs {
let projects = env
.api
.get_projects(&[alpha_project_id, beta_project_id], pat)
.await;
let projects: Vec<CommonProject> =
test::read_body_json(projects).await;
assert_eq!(projects.len(), expected_count);
}
},
)
.await;
}
// Route tests:
// TODO: Missing routes on projects
// TODO: using permissions/scopes, can we SEE projects existence that we are not allowed to? (ie 401 instead of 404)
// Permissions:
// TODO: permissions VIEW_PAYOUTS currently is unused. Add tests when it is used.
// TODO: permissions VIEW_ANALYTICS currently is unused. Add tests when it is used.