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::().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| 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::(); 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::().ok()), None ); assert_eq!( redis_pool .get(PROJECTS_SLUGS_NAMESPACE, &id) .await .unwrap() .and_then(|x| x.parse::().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| 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| 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| 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| 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::>() .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| 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| 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| 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| 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 = 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.