use actix_http::StatusCode; use actix_web::test; use bytes::Bytes; use chrono::{Duration, Utc}; use common::database::*; use common::dummy_data::DUMMY_CATEGORIES; use common::environment::{with_test_environment, TestEnvironment}; use common::permissions::{PermissionsTest, PermissionsTestContext}; use futures::StreamExt; use labrinth::database::models::project_item::{PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE}; use labrinth::models::ids::base62_impl::parse_base62; use labrinth::models::teams::ProjectPermissions; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; use serde_json::json; mod common; #[actix_rt::test] async fn test_get_project() { // Test setup and dummy data let test_env = TestEnvironment::build(None).await; let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let beta_project_id = &test_env.dummy.as_ref().unwrap().project_beta.project_id; let alpha_project_slug = &test_env.dummy.as_ref().unwrap().project_alpha.project_slug; let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; // Perform request on dummy data let req = test::TestRequest::get() .uri(&format!("/v3/project/{alpha_project_id}")) .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req).await; let status = resp.status(); let body: serde_json::Value = test::read_body_json(resp).await; assert_eq!(status, 200); 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["inner"]["slug"], json!(alpha_project_slug)); // Make the request again, this time it should be cached let req = test::TestRequest::get() .uri(&format!("/v3/project/{alpha_project_id}")) .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req).await; let status = resp.status(); assert_eq!(status, 200); 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 req = test::TestRequest::get() .uri("/v3/project/nonexistent") .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 404); // 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 req = test::TestRequest::get() .uri(&format!("/v3/project/{beta_project_id}")) .append_header(("Authorization", ENEMY_USER_PAT)) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 404); // Cleanup test db test_env.cleanup().await; } #[actix_rt::test] async fn test_add_remove_project() { // Test setup and dummy data let test_env = TestEnvironment::build(None).await; let api = &test_env.v3; // Generate test project data. let mut json_data = json!( { "title": "Test_Add_Project project", "slug": "demo", "description": "Example description.", "body": "Example body.", "initial_versions": [{ "file_parts": ["basic-mod.jar"], "version_number": "1.2.3", "version_title": "start", "dependencies": [], "game_versions": ["1.20.1"] , "client_side": "required", "server_side": "optional", "release_channel": "release", "loaders": ["fabric"], "featured": true }], "categories": [], "license_id": "MIT" } ); // 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() }; // Basic file let file_segment = MultipartSegment { name: "basic-mod.jar".to_string(), filename: Some("basic-mod.jar".to_string()), content_type: Some("application/java-archive".to_string()), // TODO: look at these: can be used in the reuse data data: MultipartSegmentData::Binary(include_bytes!("../tests/files/basic-mod.jar").to_vec()), }; // Differently named file, with the same content (for hash testing) let file_diff_name_segment = MultipartSegment { name: "basic-mod-different.jar".to_string(), filename: Some("basic-mod-different.jar".to_string()), content_type: Some("application/java-archive".to_string()), data: MultipartSegmentData::Binary(include_bytes!("../tests/files/basic-mod.jar").to_vec()), }; // Differently named file, with different content let file_diff_name_content_segment = MultipartSegment { name: "basic-mod-different.jar".to_string(), filename: Some("basic-mod-different.jar".to_string()), content_type: Some("application/java-archive".to_string()), data: MultipartSegmentData::Binary( include_bytes!("../tests/files/basic-mod-different.jar").to_vec(), ), }; // Add a project- simple, should work. let req = test::TestRequest::post() .uri("/v3/project") .append_header(("Authorization", USER_USER_PAT)) .set_multipart(vec![json_segment.clone(), file_segment.clone()]) .to_request(); let resp = test_env.call(req).await; let status = resp.status(); assert_eq!(status, 200); // Get the project we just made, and confirm that it's correct let project = api.get_project_deserialized("demo", USER_USER_PAT).await; assert!(project.versions.len() == 1); let uploaded_version_id = project.versions[0]; // Checks files to ensure they were uploaded and correctly identify the file let hash = sha1::Sha1::from(include_bytes!("../tests/files/basic-mod.jar")) .digest() .to_string(); let version = api .get_version_from_hash_deserialized(&hash, "sha1", USER_USER_PAT) .await; assert_eq!(version.id, uploaded_version_id); // Reusing with a different slug and the same file should fail // Even if that file is named differently let req = test::TestRequest::post() .uri("/v3/project") .append_header(("Authorization", USER_USER_PAT)) .set_multipart(vec![ json_diff_slug_file_segment.clone(), // Different slug, different file name file_diff_name_segment.clone(), // Different file name, same content ]) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 400); // Reusing with the same slug and a different file should fail let req = test::TestRequest::post() .uri("/v3/project") .append_header(("Authorization", USER_USER_PAT)) .set_multipart(vec![ json_diff_file_segment.clone(), // Same slug, different file name file_diff_name_content_segment.clone(), // Different file name, different content ]) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 400); // Different slug, different file should succeed let req = test::TestRequest::post() .uri("/v3/project") .append_header(("Authorization", USER_USER_PAT)) .set_multipart(vec![ json_diff_slug_file_segment.clone(), // Different slug, different file name file_diff_name_content_segment.clone(), // Different file name, same content ]) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 200); // Get let project = api.get_project_deserialized("demo", USER_USER_PAT).await; let id = project.id.to_string(); // Remove the project let resp = test_env.v3.remove_project("demo", USER_USER_PAT).await; assert_eq!(resp.status(), 204); // 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_eq!(resp.status(), 404); // Cleanup test db test_env.cleanup().await; } #[actix_rt::test] pub async fn test_patch_project() { let test_env = TestEnvironment::build(None).await; let api = &test_env.v3; let alpha_project_slug = &test_env.dummy.as_ref().unwrap().project_alpha.project_slug; let beta_project_slug = &test_env.dummy.as_ref().unwrap().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!({ "title": "Test_Add_Project project - test 1", }), ENEMY_USER_PAT, ) .await; assert_eq!(resp.status(), 401); // Failure because we are setting URL fields to invalid urls. for url_type in ["issues_url", "source_url", "wiki_url", "discord_url"] { let resp = api .edit_project( alpha_project_slug, json!({ url_type: "w.fake.url", }), USER_USER_PAT, ) .await; assert_eq!(resp.status(), 400); } // 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_eq!(resp.status(), 400); } // 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_eq!(resp.status(), 401); // (should work for a mod, though) let resp = api .edit_project( alpha_project_slug, json!({ key: "test", }), MOD_USER_PAT, ) .await; assert_eq!(resp.status(), 204); } // 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_eq!(resp.status(), 400); } // 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_eq!(resp.status(), 401); // Sucessful request to patch many fields. let resp = api .edit_project( alpha_project_slug, json!({ "slug": "newslug", "title": "New successful title", "description": "New successful description", "body": "New successful body", "categories": [DUMMY_CATEGORIES[0]], "license_id": "MIT", "issues_url": "https://github.com", "discord_url": "https://discord.gg", "wiki_url": "https://wiki.com", "donation_urls": [{ "id": "patreon", "platform": "Patreon", "url": "https://patreon.com" }] }), USER_USER_PAT, ) .await; assert_eq!(resp.status(), 204); // Old slug no longer works let resp = api.get_project(alpha_project_slug, USER_USER_PAT).await; assert_eq!(resp.status(), 404); // New slug does work let project = api.get_project_deserialized("newslug", USER_USER_PAT).await; assert_eq!(project.slug.unwrap(), "newslug"); assert_eq!(project.title, "New successful title"); assert_eq!(project.description, "New successful description"); assert_eq!(project.body, "New successful body"); assert_eq!(project.categories, vec![DUMMY_CATEGORIES[0]]); assert_eq!(project.license.id, "MIT"); assert_eq!(project.issues_url, Some("https://github.com".to_string())); assert_eq!(project.discord_url, Some("https://discord.gg".to_string())); assert_eq!(project.wiki_url, Some("https://wiki.com".to_string())); assert_eq!(project.donation_urls.unwrap()[0].url, "https://patreon.com"); // TODO: // for loader fields? // Cleanup test db test_env.cleanup().await; } #[actix_rt::test] pub async fn test_bulk_edit_categories() { with_test_environment(|test_env| async move { let api = &test_env.v3; 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; 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_eq!(resp.status(), StatusCode::NO_CONTENT); let alpha_body = api .get_project_deserialized(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(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() { let test_env = TestEnvironment::build(Some(8)).await; let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; // 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 ("title", json!("randomname")), ("description", json!("randomdescription")), ("categories", json!(["combat", "economy"])), ("additional_categories", json!(["decoration"])), ("issues_url", json!("https://issues.com")), ("source_url", json!("https://source.com")), ("wiki_url", json!("https://wiki.com")), ( "donation_urls", json!([{ "id": "paypal", "platform": "Paypal", "url": "https://paypal.com" }]), ), ("discord_url", json!("https://discord.com")), ("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| { test::TestRequest::patch() .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) .set_json(json!({ key: if key == "slug" { json!(generate_random_name("randomslug")) } else { value.clone() }, })) }; 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| { test::TestRequest::patch() .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) .set_json(json!({ "status": "private", "requested_status": "private", })) }; 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| { test::TestRequest::patch() .uri(&format!( "/v3/projects?ids=[{uri}]", uri = urlencoding::encode(&format!("\"{}\"", ctx.project_id.unwrap())) )) .set_json(json!({ "name": "randomname", })) }; 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| { test::TestRequest::patch() .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) .set_json(json!({ "body": "new body!", })) }; PermissionsTest::new(&test_env) .simple_project_permissions_test(edit_body, req_gen) .await .unwrap(); test_env.cleanup().await; } // Not covered by PATCH /project #[actix_rt::test] async fn permissions_edit_details() { let test_env = TestEnvironment::build(None).await; let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; let beta_project_id = &test_env.dummy.as_ref().unwrap().project_beta.project_id; let beta_team_id = &test_env.dummy.as_ref().unwrap().project_beta.team_id; let beta_version_id = &test_env.dummy.as_ref().unwrap().project_beta.version_id; let edit_details = ProjectPermissions::EDIT_DETAILS; // Approve beta version as private so we can schedule it let req = test::TestRequest::patch() .uri(&format!("/v3/version/{beta_version_id}")) .append_header(("Authorization", MOD_USER_PAT)) .set_json(json!({ "status": "unlisted" })) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 204); // Schedule version let req_gen = |_: &PermissionsTestContext| { test::TestRequest::post() .uri(&format!("/v3/version/{beta_version_id}/schedule")) // beta_version_id is an *approved* version, so we can schedule it .set_json(json!( { "requested_status": "archived", "time": Utc::now() + Duration::days(1), } )) }; 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(); // Icon edit // Uses alpha project to delete this icon let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() .uri(&format!( "/v3/project/{}/icon?ext=png", ctx.project_id.unwrap() )) .set_payload(Bytes::from( include_bytes!("../tests/files/200x200.png") as &[u8] )) }; 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| { test::TestRequest::delete().uri(&format!( "/v3/project/{}/icon?ext=png", ctx.project_id.unwrap() )) }; 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| { test::TestRequest::post() .uri(&format!( "/v3/project/{}/gallery?ext=png&featured=true", ctx.project_id.unwrap() )) .set_payload(Bytes::from( include_bytes!("../tests/files/200x200.png") as &[u8] )) }; 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 req = test::TestRequest::get() .uri(&format!("/v3/project/{alpha_project_id}")) .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req).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| { test::TestRequest::patch().uri(&format!( "/v3/project/{}/gallery?url={gallery_url}", ctx.project_id.unwrap() )) }; 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| { test::TestRequest::delete().uri(&format!( "/v3/project/{}/gallery?url={gallery_url}", ctx.project_id.unwrap() )) }; 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(); } #[actix_rt::test] async fn permissions_upload_version() { let test_env = TestEnvironment::build(None).await; let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let alpha_version_id = &test_env.dummy.as_ref().unwrap().project_alpha.version_id; let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; let alpha_file_hash = &test_env.dummy.as_ref().unwrap().project_alpha.file_hash; let upload_version = ProjectPermissions::UPLOAD_VERSION; // Upload version with basic-mod.jar let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::post().uri("/v3/version").set_multipart([ MultipartSegment { name: "data".to_string(), filename: None, content_type: Some("application/json".to_string()), data: MultipartSegmentData::Text( serde_json::to_string(&json!({ "project_id": ctx.project_id.unwrap(), "file_parts": ["basic-mod.jar"], "version_number": "1.0.0", "version_title": "1.0.0", "version_type": "release", "client_side": "required", "server_side": "optional", "dependencies": [], "game_versions": ["1.20.1"], "loaders": ["fabric"], "featured": false, })) .unwrap(), ), }, MultipartSegment { name: "basic-mod.jar".to_string(), filename: Some("basic-mod.jar".to_string()), content_type: Some("application/java-archive".to_string()), data: MultipartSegmentData::Binary( include_bytes!("../tests/files/basic-mod.jar").to_vec(), ), }, ]) }; 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 = |_: &PermissionsTestContext| { test::TestRequest::post() .uri(&format!("/v3/version/{}/file", alpha_version_id)) .set_multipart([ MultipartSegment { name: "data".to_string(), filename: None, content_type: Some("application/json".to_string()), data: MultipartSegmentData::Text( serde_json::to_string(&json!({ "file_parts": ["basic-mod-different.jar"], })) .unwrap(), ), }, MultipartSegment { name: "basic-mod-different.jar".to_string(), filename: Some("basic-mod-different.jar".to_string()), content_type: Some("application/java-archive".to_string()), data: MultipartSegmentData::Binary( include_bytes!("../tests/files/basic-mod-different.jar").to_vec(), ), }, ]) }; 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 = |_: &PermissionsTestContext| { test::TestRequest::patch() .uri(&format!("/v3/version/{}", alpha_version_id)) .set_json(json!({ "name": "Basic Mod", })) }; 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 = |_: &PermissionsTestContext| { test::TestRequest::delete().uri(&format!("/v3/version_file/{}", alpha_file_hash)) }; 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 = |_: &PermissionsTestContext| { test::TestRequest::delete().uri(&format!("/v3/version/{}", alpha_version_id)) }; 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(); test_env.cleanup().await; } #[actix_rt::test] async fn permissions_manage_invites() { // Add member, remove member, edit member let test_env = TestEnvironment::build(None).await; let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; let manage_invites = ProjectPermissions::MANAGE_INVITES; // Add member let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::post() .uri(&format!("/v3/team/{}/members", ctx.team_id.unwrap())) .set_json(json!({ "user_id": MOD_USER_ID, "permissions": 0, })) }; 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| { test::TestRequest::patch() .uri(&format!( "/v3/team/{}/members/{MOD_USER_ID}", ctx.team_id.unwrap() )) .set_json(json!({ "permissions": 0, })) }; 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| { test::TestRequest::delete().uri(&format!( "/v3/team/{}/members/{MOD_USER_ID}", ctx.team_id.unwrap() )) }; 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 req = test::TestRequest::post() .uri(&format!("/v3/team/{}/members", alpha_team_id)) .append_header(("Authorization", ADMIN_USER_PAT)) .set_json(json!({ "user_id": MOD_USER_ID, })) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 204); // Accept invite let req = test::TestRequest::post() .uri(&format!("/v3/team/{}/join", alpha_team_id)) .append_header(("Authorization", MOD_USER_PAT)) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 204); // remove existing member (requires remove_member) let remove_member = ProjectPermissions::REMOVE_MEMBER; let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::delete().uri(&format!( "/v3/team/{}/members/{MOD_USER_ID}", ctx.team_id.unwrap() )) }; 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(); test_env.cleanup().await; } #[actix_rt::test] async fn permissions_delete_project() { // Add member, remove member, edit member let test_env = TestEnvironment::build(None).await; let delete_project = ProjectPermissions::DELETE_PROJECT; // Delete project let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::delete().uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) }; PermissionsTest::new(&test_env) .simple_project_permissions_test(delete_project, req_gen) .await .unwrap(); test_env.cleanup().await; } #[actix_rt::test] async fn project_permissions_consistency_test() { let test_env = TestEnvironment::build(Some(10)).await; // 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 // Full project permissions test with EDIT_DETAILS let success_permissions = ProjectPermissions::EDIT_DETAILS; let req_gen = |ctx: &PermissionsTestContext| { test::TestRequest::patch() .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) .set_json(json!({ "title": "Example title - changed.", })) }; 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| { test::TestRequest::patch() .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) .set_json(json!({ "title": "Example title - changed.", })) }; PermissionsTest::new(&test_env) .full_project_permissions_test(success_permissions, req_gen) .await .unwrap(); test_env.cleanup().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.