diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 93e6a72d..8bd7745a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - rust: [beta, nightly, stable] + rust: [stable] steps: - uses: actions/checkout@v2 diff --git a/src/auth/flows.rs b/src/auth/flows.rs index 7b54f5b1..8572c29a 100644 --- a/src/auth/flows.rs +++ b/src/auth/flows.rs @@ -2247,7 +2247,11 @@ pub async fn link_trolley( } if let Some(email) = user.email { - let id = payouts_queue.lock().await.register_recipient(&email, body.0).await?; + let id = payouts_queue + .lock() + .await + .register_recipient(&email, body.0) + .await?; let mut transaction = pool.begin().await?; diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index 7a0d753f..a3bc2da6 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -380,7 +380,7 @@ impl User { redis .delete_many( user_ids - .into_iter() + .iter() .map(|id| (USERS_PROJECTS_NAMESPACE, Some(id.0.to_string()))), ) .await?; diff --git a/src/models/teams.rs b/src/models/teams.rs index 9182474d..a77281f6 100644 --- a/src/models/teams.rs +++ b/src/models/teams.rs @@ -23,7 +23,7 @@ pub struct Team { } bitflags::bitflags! { - #[derive(Copy, Clone, Debug)] + #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct ProjectPermissions: u64 { const UPLOAD_VERSION = 1 << 0; const DELETE_VERSION = 1 << 1; @@ -35,8 +35,6 @@ bitflags::bitflags! { const DELETE_PROJECT = 1 << 7; const VIEW_ANALYTICS = 1 << 8; const VIEW_PAYOUTS = 1 << 9; - - const ALL = 0b1111111111; } } @@ -55,15 +53,19 @@ impl ProjectPermissions { organization_team_member: &Option, // team member of the user in the organization ) -> Option { if role.is_admin() { - return Some(ProjectPermissions::ALL); + return Some(ProjectPermissions::all()); } if let Some(member) = project_team_member { - return Some(member.permissions); + if member.accepted { + return Some(member.permissions); + } } if let Some(member) = organization_team_member { - return Some(member.permissions); // Use default project permissions for the organization team member + if member.accepted { + return Some(member.permissions); + } } if role.is_mod() { @@ -79,18 +81,16 @@ impl ProjectPermissions { } bitflags::bitflags! { - #[derive(Copy, Clone, Debug)] + #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct OrganizationPermissions: u64 { const EDIT_DETAILS = 1 << 0; - const EDIT_BODY = 1 << 1; - const MANAGE_INVITES = 1 << 2; - const REMOVE_MEMBER = 1 << 3; - const EDIT_MEMBER = 1 << 4; - const ADD_PROJECT = 1 << 5; - const REMOVE_PROJECT = 1 << 6; - const DELETE_ORGANIZATION = 1 << 8; - const EDIT_MEMBER_DEFAULT_PERMISSIONS = 1 << 9; // Separate from EDIT_MEMBER - const ALL = 0b1111111111; + const MANAGE_INVITES = 1 << 1; + const REMOVE_MEMBER = 1 << 2; + const EDIT_MEMBER = 1 << 3; + const ADD_PROJECT = 1 << 4; + const REMOVE_PROJECT = 1 << 5; + const DELETE_ORGANIZATION = 1 << 6; + const EDIT_MEMBER_DEFAULT_PERMISSIONS = 1 << 7; // Separate from EDIT_MEMBER const NONE = 0b0; } } @@ -109,17 +109,17 @@ impl OrganizationPermissions { team_member: &Option, ) -> Option { if role.is_admin() { - return Some(OrganizationPermissions::ALL); + return Some(OrganizationPermissions::all()); } if let Some(member) = team_member { - return member.organization_permissions; + if member.accepted { + return member.organization_permissions; + } } if role.is_mod() { return Some( - OrganizationPermissions::EDIT_DETAILS - | OrganizationPermissions::EDIT_BODY - | OrganizationPermissions::ADD_PROJECT, + OrganizationPermissions::EDIT_DETAILS | OrganizationPermissions::ADD_PROJECT, ); } None diff --git a/src/routes/maven.rs b/src/routes/maven.rs index ae19b7cf..d3e7a3c5 100644 --- a/src/routes/maven.rs +++ b/src/routes/maven.rs @@ -206,7 +206,11 @@ async fn find_version( }) .collect::>(); - Ok(matched.get(0).or_else(|| exact_matches.get(0)).copied().cloned()) + Ok(matched + .get(0) + .or_else(|| exact_matches.get(0)) + .copied() + .cloned()) } fn find_file<'a>( diff --git a/src/routes/v2/admin.rs b/src/routes/v2/admin.rs index cc5bd9e6..d5d03059 100644 --- a/src/routes/v2/admin.rs +++ b/src/routes/v2/admin.rs @@ -120,7 +120,7 @@ pub async fn count_download( analytics_queue.add_download(Download { id: Uuid::new_v4(), - recorded: get_current_tenths_of_ms(), + recorded: get_current_tenths_of_ms(), domain: url.host_str().unwrap_or_default().to_string(), site_path: url.path().to_string(), user_id: user diff --git a/src/routes/v2/organizations.rs b/src/routes/v2/organizations.rs index 754d1a1e..d4c8a056 100644 --- a/src/routes/v2/organizations.rs +++ b/src/routes/v2/organizations.rs @@ -94,8 +94,8 @@ pub async fn organization_create( members: vec![team_item::TeamMemberBuilder { user_id: current_user.id.into(), role: crate::models::teams::OWNER_ROLE.to_owned(), - permissions: ProjectPermissions::ALL, - organization_permissions: Some(OrganizationPermissions::ALL), + permissions: ProjectPermissions::all(), + organization_permissions: Some(OrganizationPermissions::all()), accepted: true, payouts_split: Decimal::ONE_HUNDRED, ordering: 0, diff --git a/src/routes/v2/teams.rs b/src/routes/v2/teams.rs index 14cabdf0..254e6609 100644 --- a/src/routes/v2/teams.rs +++ b/src/routes/v2/teams.rs @@ -407,12 +407,10 @@ pub async fn add_team_member( ) .await? .1; - let team_association = Team::get_association(team_id, &**pool) .await? .ok_or_else(|| ApiError::InvalidInput("The team specified does not exist".to_string()))?; let member = TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool).await?; - match team_association { // If team is associated with a project, check if they have permissions to invite users to that project TeamAssociationId::Project(pid) => { @@ -470,8 +468,8 @@ pub async fn add_team_member( .contains(OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS) && !new_member.permissions.is_empty() { - return Err(ApiError::InvalidInput( - "You do not have permission to give this user default project permissions." + return Err(ApiError::CustomAuthentication( + "You do not have permission to give this user default project permissions. Ensure 'permissions' is set if it is not, and empty (0)." .to_string(), )); } @@ -654,8 +652,8 @@ pub async fn edit_team_member( .unwrap_or_default(); if !organization_permissions.contains(OrganizationPermissions::EDIT_MEMBER) { - return Err(ApiError::InvalidInput( - "You don't have permission to edit organization permissions".to_string(), + return Err(ApiError::CustomAuthentication( + "You don't have permission to edit members of this team".to_string(), )); } @@ -672,7 +670,7 @@ pub async fn edit_team_member( && !organization_permissions .contains(OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS) { - return Err(ApiError::InvalidInput( + return Err(ApiError::CustomAuthentication( "You do not have permission to give this user default project permissions." .to_string(), )); @@ -884,7 +882,6 @@ pub async fn remove_team_member( // removed by a member with the REMOVE_MEMBER permission. if Some(delete_member.user_id) == member.as_ref().map(|m| m.user_id) || permissions.contains(ProjectPermissions::REMOVE_MEMBER) - && member.as_ref().map(|m| m.accepted).unwrap_or(true) // true as if the permission exists, but the member does not, they are part of an org { TeamMember::delete(id, user_id, &mut transaction).await?; @@ -896,7 +893,6 @@ pub async fn remove_team_member( } } else if Some(delete_member.user_id) == member.as_ref().map(|m| m.user_id) || permissions.contains(ProjectPermissions::MANAGE_INVITES) - && member.as_ref().map(|m| m.accepted).unwrap_or(true) // true as if the permission exists, but the member does not, they are part of an org { // This is a pending invite rather than a member, so the @@ -913,49 +909,37 @@ pub async fn remove_team_member( let organization_permissions = OrganizationPermissions::get_permissions_by_role(¤t_user.role, &member) .unwrap_or_default(); - if let Some(member) = member { - // Organization teams requires a TeamMember, so we can 'unwrap' - if delete_member.accepted { - // Members other than the owner can either leave the team, or be - // removed by a member with the REMOVE_MEMBER permission. - if delete_member.user_id == member.user_id - || organization_permissions - .contains(OrganizationPermissions::REMOVE_MEMBER) - && member.accepted - { - TeamMember::delete(id, user_id, &mut transaction).await?; - } else { - return Err(ApiError::CustomAuthentication( - "You do not have permission to remove a member from this organization" - .to_string(), - )); - } - } else if delete_member.user_id == member.user_id - || organization_permissions - .contains(OrganizationPermissions::MANAGE_INVITES) - && member.accepted + // Organization teams requires a TeamMember, so we can 'unwrap' + if delete_member.accepted { + // Members other than the owner can either leave the team, or be + // removed by a member with the REMOVE_MEMBER permission. + if Some(delete_member.user_id) == member.map(|m| m.user_id) + || organization_permissions.contains(OrganizationPermissions::REMOVE_MEMBER) { - // This is a pending invite rather than a member, so the - // user being invited or team members with the MANAGE_INVITES - // permission can remove it. TeamMember::delete(id, user_id, &mut transaction).await?; } else { return Err(ApiError::CustomAuthentication( - "You do not have permission to cancel an organization invite" + "You do not have permission to remove a member from this organization" .to_string(), )); } + } else if Some(delete_member.user_id) == member.map(|m| m.user_id) + || organization_permissions.contains(OrganizationPermissions::MANAGE_INVITES) + { + // This is a pending invite rather than a member, so the + // user being invited or team members with the MANAGE_INVITES + // permission can remove it. + TeamMember::delete(id, user_id, &mut transaction).await?; } else { return Err(ApiError::CustomAuthentication( - "You do not have permission to remove a member from this organization" - .to_string(), + "You do not have permission to cancel an organization invite".to_string(), )); } } } TeamMember::clear_cache(id, &redis).await?; - User::clear_project_cache(&[delete_member.user_id.into()], &redis).await?; + User::clear_project_cache(&[delete_member.user_id], &redis).await?; transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) diff --git a/tests/common/actix.rs b/tests/common/actix.rs index 03935e50..11759d7f 100644 --- a/tests/common/actix.rs +++ b/tests/common/actix.rs @@ -18,11 +18,11 @@ pub enum MultipartSegmentData { } pub trait AppendsMultipart { - fn set_multipart(self, data: Vec) -> Self; + fn set_multipart(self, data: impl IntoIterator) -> Self; } impl AppendsMultipart for TestRequest { - fn set_multipart(self, data: Vec) -> Self { + fn set_multipart(self, data: impl IntoIterator) -> Self { let (boundary, payload) = generate_multipart(data); self.append_header(( "Content-Type", @@ -32,7 +32,7 @@ impl AppendsMultipart for TestRequest { } } -fn generate_multipart(data: Vec) -> (String, Bytes) { +fn generate_multipart(data: impl IntoIterator) -> (String, Bytes) { let mut boundary = String::from("----WebKitFormBoundary"); boundary.push_str(&rand::random::().to_string()); boundary.push_str(&rand::random::().to_string()); diff --git a/tests/common/api_v2/mod.rs b/tests/common/api_v2/mod.rs new file mode 100644 index 00000000..2ecc144e --- /dev/null +++ b/tests/common/api_v2/mod.rs @@ -0,0 +1,20 @@ +#![allow(dead_code)] + +use super::environment::LocalService; +use actix_web::dev::ServiceResponse; +use std::rc::Rc; + +pub mod organization; +pub mod project; +pub mod team; + +#[derive(Clone)] +pub struct ApiV2 { + pub test_app: Rc, +} + +impl ApiV2 { + pub async fn call(&self, req: actix_http::Request) -> ServiceResponse { + self.test_app.call(req).await.unwrap() + } +} diff --git a/tests/common/api_v2/organization.rs b/tests/common/api_v2/organization.rs new file mode 100644 index 00000000..31f0ea4c --- /dev/null +++ b/tests/common/api_v2/organization.rs @@ -0,0 +1,152 @@ +use actix_web::{ + dev::ServiceResponse, + test::{self, TestRequest}, +}; +use bytes::Bytes; +use labrinth::models::{organizations::Organization, projects::Project}; +use serde_json::json; + +use crate::common::request_data::ImageData; + +use super::ApiV2; + +impl ApiV2 { + pub async fn create_organization( + &self, + organization_title: &str, + description: &str, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::post() + .uri("/v2/organization") + .append_header(("Authorization", pat)) + .set_json(json!({ + "title": organization_title, + "description": description, + })) + .to_request(); + self.call(req).await + } + + pub async fn get_organization(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + let req = TestRequest::get() + .uri(&format!("/v2/organization/{id_or_title}")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn get_organization_deserialized( + &self, + id_or_title: &str, + pat: &str, + ) -> Organization { + let resp = self.get_organization(id_or_title, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn get_organization_projects(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::get() + .uri(&format!("/v2/organization/{id_or_title}/projects")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn get_organization_projects_deserialized( + &self, + id_or_title: &str, + pat: &str, + ) -> Vec { + let resp = self.get_organization_projects(id_or_title, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn edit_organization( + &self, + id_or_title: &str, + patch: serde_json::Value, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::patch() + .uri(&format!("/v2/organization/{id_or_title}")) + .append_header(("Authorization", pat)) + .set_json(patch) + .to_request(); + + self.call(req).await + } + + pub async fn edit_organization_icon( + &self, + id_or_title: &str, + icon: Option, + pat: &str, + ) -> ServiceResponse { + if let Some(icon) = icon { + // If an icon is provided, upload it + let req = test::TestRequest::patch() + .uri(&format!( + "/v2/organization/{id_or_title}/icon?ext={ext}", + ext = icon.extension + )) + .append_header(("Authorization", pat)) + .set_payload(Bytes::from(icon.icon)) + .to_request(); + + self.call(req).await + } else { + // If no icon is provided, delete the icon + let req = test::TestRequest::delete() + .uri(&format!("/v2/organization/{id_or_title}/icon")) + .append_header(("Authorization", pat)) + .to_request(); + + self.call(req).await + } + } + + pub async fn delete_organization(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::delete() + .uri(&format!("/v2/organization/{id_or_title}")) + .append_header(("Authorization", pat)) + .to_request(); + + self.call(req).await + } + + pub async fn organization_add_project( + &self, + id_or_title: &str, + project_id_or_slug: &str, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::post() + .uri(&format!("/v2/organization/{id_or_title}/projects")) + .append_header(("Authorization", pat)) + .set_json(json!({ + "project_id": project_id_or_slug, + })) + .to_request(); + + self.call(req).await + } + + pub async fn organization_remove_project( + &self, + id_or_title: &str, + project_id_or_slug: &str, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::delete() + .uri(&format!( + "/v2/organization/{id_or_title}/projects/{project_id_or_slug}" + )) + .append_header(("Authorization", pat)) + .to_request(); + + self.call(req).await + } +} diff --git a/tests/common/api_v2.rs b/tests/common/api_v2/project.rs similarity index 54% rename from tests/common/api_v2.rs rename to tests/common/api_v2/project.rs index 3bd98576..d8f5f858 100644 --- a/tests/common/api_v2.rs +++ b/tests/common/api_v2/project.rs @@ -1,41 +1,31 @@ -#![allow(dead_code)] - -use super::{ - actix::AppendsMultipart, - asserts::assert_status, - database::{MOD_USER_PAT, USER_USER_PAT}, - environment::LocalService, - request_data::ProjectCreationRequestData, -}; use actix_http::StatusCode; use actix_web::{ dev::ServiceResponse, test::{self, TestRequest}, }; -use labrinth::models::{ - notifications::Notification, - projects::{Project, Version}, -}; +use bytes::Bytes; +use labrinth::models::projects::{Project, Version}; use serde_json::json; -use std::rc::Rc; -pub struct ApiV2 { - pub test_app: Rc>, -} +use crate::common::{ + actix::AppendsMultipart, + asserts::assert_status, + database::MOD_USER_PAT, + request_data::{ImageData, ProjectCreationRequestData}, +}; -impl ApiV2 { - pub async fn call(&self, req: actix_http::Request) -> ServiceResponse { - self.test_app.call(req).await.unwrap() - } +use super::ApiV2; +impl ApiV2 { pub async fn add_public_project( &self, creation_data: ProjectCreationRequestData, - ) -> (Project, Version) { + pat: &str, + ) -> (Project, Vec) { // Add a project. let req = TestRequest::post() .uri("/v2/project") - .append_header(("Authorization", USER_USER_PAT)) + .append_header(("Authorization", pat)) .set_multipart(creation_data.segment_data) .to_request(); let resp = self.call(req).await; @@ -55,19 +45,18 @@ impl ApiV2 { assert_status(resp, StatusCode::NO_CONTENT); let project = self - .get_project_deserialized(&creation_data.slug, USER_USER_PAT) + .get_project_deserialized(&creation_data.slug, pat) .await; // Get project's versions let req = TestRequest::get() .uri(&format!("/v2/project/{}/version", creation_data.slug)) - .append_header(("Authorization", USER_USER_PAT)) + .append_header(("Authorization", pat)) .to_request(); let resp = self.call(req).await; let versions: Vec = test::read_body_json(resp).await; - let version = versions.into_iter().next().unwrap(); - (project, version) + (project, versions) } pub async fn remove_project(&self, project_slug_or_id: &str, pat: &str) -> ServiceResponse { @@ -80,12 +69,16 @@ impl ApiV2 { resp } - pub async fn get_project_deserialized(&self, slug: &str, pat: &str) -> Project { + pub async fn get_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse { let req = TestRequest::get() - .uri(&format!("/v2/project/{slug}")) + .uri(&format!("/v2/project/{id_or_slug}")) .append_header(("Authorization", pat)) .to_request(); - let resp = self.call(req).await; + self.call(req).await + } + pub async fn get_project_deserialized(&self, id_or_slug: &str, pat: &str) -> Project { + let resp = self.get_project(id_or_slug, pat).await; + assert_eq!(resp.status(), 200); test::read_body_json(resp).await } @@ -103,73 +96,94 @@ impl ApiV2 { test::read_body_json(resp).await } - pub async fn add_user_to_team( + pub async fn get_version_from_hash( &self, - team_id: &str, - user_id: &str, + hash: &str, + algorithm: &str, pat: &str, ) -> ServiceResponse { - let req = test::TestRequest::post() - .uri(&format!("/v2/team/{team_id}/members")) - .append_header(("Authorization", pat)) - .set_json(json!( { - "user_id": user_id - })) - .to_request(); - self.call(req).await - } - - pub async fn join_team(&self, team_id: &str, pat: &str) -> ServiceResponse { - let req = test::TestRequest::post() - .uri(&format!("/v2/team/{team_id}/join")) + let req = test::TestRequest::get() + .uri(&format!("/v2/version_file/{hash}?algorithm={algorithm}")) .append_header(("Authorization", pat)) .to_request(); self.call(req).await } - pub async fn remove_from_team( + pub async fn get_version_from_hash_deserialized( &self, - team_id: &str, - user_id: &str, + hash: &str, + algorithm: &str, pat: &str, - ) -> ServiceResponse { - let req = test::TestRequest::delete() - .uri(&format!("/v2/team/{team_id}/members/{user_id}")) - .append_header(("Authorization", pat)) - .to_request(); - self.call(req).await + ) -> Version { + let resp = self.get_version_from_hash(hash, algorithm, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await } - pub async fn get_user_notifications_deserialized( + pub async fn edit_project( &self, - user_id: &str, + id_or_slug: &str, + patch: serde_json::Value, pat: &str, - ) -> Vec { - let req = test::TestRequest::get() - .uri(&format!("/v2/user/{user_id}/notifications")) + ) -> ServiceResponse { + let req = test::TestRequest::patch() + .uri(&format!("/v2/project/{id_or_slug}")) .append_header(("Authorization", pat)) + .set_json(patch) .to_request(); - let resp = self.call(req).await; - test::read_body_json(resp).await + + self.call(req).await } - pub async fn mark_notification_read( + pub async fn edit_project_bulk( &self, - notification_id: &str, + ids_or_slugs: impl IntoIterator, + patch: serde_json::Value, pat: &str, ) -> ServiceResponse { + let projects_str = ids_or_slugs + .into_iter() + .map(|s| format!("\"{}\"", s)) + .collect::>() + .join(","); let req = test::TestRequest::patch() - .uri(&format!("/v2/notification/{notification_id}")) + .uri(&format!( + "/v2/projects?ids={encoded}", + encoded = urlencoding::encode(&format!("[{projects_str}]")) + )) .append_header(("Authorization", pat)) + .set_json(patch) .to_request(); + self.call(req).await } - pub async fn delete_notification(&self, notification_id: &str, pat: &str) -> ServiceResponse { - let req = test::TestRequest::delete() - .uri(&format!("/v2/notification/{notification_id}")) - .append_header(("Authorization", pat)) - .to_request(); - self.call(req).await + pub async fn edit_project_icon( + &self, + id_or_slug: &str, + icon: Option, + pat: &str, + ) -> ServiceResponse { + if let Some(icon) = icon { + // If an icon is provided, upload it + let req = test::TestRequest::patch() + .uri(&format!( + "/v2/project/{id_or_slug}/icon?ext={ext}", + ext = icon.extension + )) + .append_header(("Authorization", pat)) + .set_payload(Bytes::from(icon.icon)) + .to_request(); + + self.call(req).await + } else { + // If no icon is provided, delete the icon + let req = test::TestRequest::delete() + .uri(&format!("/v2/project/{id_or_slug}/icon")) + .append_header(("Authorization", pat)) + .to_request(); + + self.call(req).await + } } } diff --git a/tests/common/api_v2/team.rs b/tests/common/api_v2/team.rs new file mode 100644 index 00000000..f1d6ef73 --- /dev/null +++ b/tests/common/api_v2/team.rs @@ -0,0 +1,168 @@ +use actix_web::{dev::ServiceResponse, test}; +use labrinth::models::{ + notifications::Notification, + teams::{OrganizationPermissions, ProjectPermissions, TeamMember}, +}; +use serde_json::json; + +use super::ApiV2; + +impl ApiV2 { + pub async fn get_team_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::get() + .uri(&format!("/v2/team/{id_or_title}/members")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn get_team_members_deserialized( + &self, + id_or_title: &str, + pat: &str, + ) -> Vec { + let resp = self.get_team_members(id_or_title, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn get_project_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::get() + .uri(&format!("/v2/project/{id_or_title}/members")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn get_project_members_deserialized( + &self, + id_or_title: &str, + pat: &str, + ) -> Vec { + let resp = self.get_project_members(id_or_title, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn get_organization_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::get() + .uri(&format!("/v2/organization/{id_or_title}/members")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn get_organization_members_deserialized( + &self, + id_or_title: &str, + pat: &str, + ) -> Vec { + let resp = self.get_organization_members(id_or_title, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + + pub async fn join_team(&self, team_id: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::post() + .uri(&format!("/v2/team/{team_id}/join")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn remove_from_team( + &self, + team_id: &str, + user_id: &str, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::delete() + .uri(&format!("/v2/team/{team_id}/members/{user_id}")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + + pub async fn edit_team_member( + &self, + team_id: &str, + user_id: &str, + patch: serde_json::Value, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{team_id}/members/{user_id}")) + .append_header(("Authorization", pat)) + .set_json(patch) + .to_request(); + self.call(req).await + } + + pub async fn transfer_team_ownership( + &self, + team_id: &str, + user_id: &str, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{team_id}/owner")) + .append_header(("Authorization", pat)) + .set_json(json!({ + "user_id": user_id, + })) + .to_request(); + self.call(req).await + } + + pub async fn get_user_notifications_deserialized( + &self, + user_id: &str, + pat: &str, + ) -> Vec { + let req = test::TestRequest::get() + .uri(&format!("/v2/user/{user_id}/notifications")) + .append_header(("Authorization", pat)) + .to_request(); + let resp = self.call(req).await; + test::read_body_json(resp).await + } + + pub async fn mark_notification_read( + &self, + notification_id: &str, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::patch() + .uri(&format!("/v2/notification/{notification_id}")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } + pub async fn add_user_to_team( + &self, + team_id: &str, + user_id: &str, + project_permissions: Option, + organization_permissions: Option, + pat: &str, + ) -> ServiceResponse { + let req = test::TestRequest::post() + .uri(&format!("/v2/team/{team_id}/members")) + .append_header(("Authorization", pat)) + .set_json(json!( { + "user_id": user_id, + "permissions" : project_permissions.map(|p| p.bits()).unwrap_or_default(), + "organization_permissions" : organization_permissions.map(|p| p.bits()), + })) + .to_request(); + self.call(req).await + } + + pub async fn delete_notification(&self, notification_id: &str, pat: &str) -> ServiceResponse { + let req = test::TestRequest::delete() + .uri(&format!("/v2/notification/{notification_id}")) + .append_header(("Authorization", pat)) + .to_request(); + self.call(req).await + } +} diff --git a/tests/common/database.rs b/tests/common/database.rs index 483a44d9..e30bd077 100644 --- a/tests/common/database.rs +++ b/tests/common/database.rs @@ -5,6 +5,8 @@ use sqlx::{postgres::PgPoolOptions, PgPool}; use std::time::Duration; use url::Url; +use crate::common::{dummy_data, environment::TestEnvironment}; + // The dummy test database adds a fair bit of 'dummy' data to test with. // Some constants are used to refer to that data, and are described here. // The rest can be accessed in the TestEnvironment 'dummy' field. @@ -29,6 +31,8 @@ pub const USER_USER_PAT: &str = "mrp_patuser"; pub const FRIEND_USER_PAT: &str = "mrp_patfriend"; pub const ENEMY_USER_PAT: &str = "mrp_patenemy"; +const TEMPLATE_DATABASE_NAME: &str = "labrinth_tests_template"; + #[derive(Clone)] pub struct TemporaryDatabase { pub pool: PgPool, @@ -37,41 +41,32 @@ pub struct TemporaryDatabase { } impl TemporaryDatabase { - // Creates a temporary database like sqlx::test does + // Creates a temporary database like sqlx::test does (panics) // 1. Logs into the main database // 2. Creates a new randomly generated database // 3. Runs migrations on the new database // 4. (Optionally, by using create_with_dummy) adds dummy data to the database // If a db is created with create_with_dummy, it must be cleaned up with cleanup. // This means that dbs will only 'remain' if a test fails (for examination of the db), and will be cleaned up otherwise. - pub async fn create() -> Self { - let temp_database_name = generate_random_database_name(); + pub async fn create(max_connections: Option) -> Self { + let temp_database_name = generate_random_name("labrinth_tests_db_"); println!("Creating temporary database: {}", &temp_database_name); let database_url = dotenvy::var("DATABASE_URL").expect("No database URL"); - let mut url = Url::parse(&database_url).expect("Invalid database URL"); - let pool = PgPool::connect(&database_url) - .await - .expect("Connection to database failed"); - - // Create the temporary database - let create_db_query = format!("CREATE DATABASE {}", &temp_database_name); - sqlx::query(&create_db_query) - .execute(&pool) - .await - .expect("Database creation failed"); + // Create the temporary (and template datbase, if needed) + Self::create_temporary(&database_url, &temp_database_name).await; - pool.close().await; + // Pool to the temporary database + let mut temporary_url = Url::parse(&database_url).expect("Invalid database URL"); - // Modify the URL to switch to the temporary database - url.set_path(&format!("/{}", &temp_database_name)); - let temp_db_url = url.to_string(); + temporary_url.set_path(&format!("/{}", &temp_database_name)); + let temp_db_url = temporary_url.to_string(); let pool = PgPoolOptions::new() .min_connections(0) - .max_connections(4) - .max_lifetime(Some(Duration::from_secs(60 * 60))) + .max_connections(max_connections.unwrap_or(4)) + .max_lifetime(Some(Duration::from_secs(60))) .connect(&temp_db_url) .await .expect("Connection to temporary database failed"); @@ -94,7 +89,103 @@ impl TemporaryDatabase { } } - // Deletes the temporary database + // Creates a template and temporary databse (panics) + // 1. Waits to obtain a pg lock on the main database + // 2. Creates a new template database called 'TEMPLATE_DATABASE_NAME', if needed + // 3. Switches to the template database + // 4. Runs migrations on the new database (for most tests, this should not take time) + // 5. Creates dummy data on the new db + // 6. Creates a temporary database at 'temp_database_name' from the template + // 7. Drops lock and all created connections in the function + async fn create_temporary(database_url: &str, temp_database_name: &str) { + let main_pool = PgPool::connect(database_url) + .await + .expect("Connection to database failed"); + + loop { + // Try to acquire an advisory lock + let lock_acquired: bool = sqlx::query_scalar("SELECT pg_try_advisory_lock(1)") + .fetch_one(&main_pool) + .await + .unwrap(); + + if lock_acquired { + // Create the db template if it doesn't exist + // Check if template_db already exists + let db_exists: Option = sqlx::query_scalar(&format!( + "SELECT 1 FROM pg_database WHERE datname = '{TEMPLATE_DATABASE_NAME}'" + )) + .fetch_optional(&main_pool) + .await + .unwrap(); + if db_exists.is_none() { + let create_db_query = format!("CREATE DATABASE {TEMPLATE_DATABASE_NAME}"); + sqlx::query(&create_db_query) + .execute(&main_pool) + .await + .expect("Database creation failed"); + } + + // Switch to template + let url = dotenvy::var("DATABASE_URL").expect("No database URL"); + let mut template_url = Url::parse(&url).expect("Invalid database URL"); + template_url.set_path(&format!("/{}", TEMPLATE_DATABASE_NAME)); + + let pool = PgPool::connect(template_url.as_str()) + .await + .expect("Connection to database failed"); + + // Run migrations on the template + let migrations = sqlx::migrate!("./migrations"); + migrations.run(&pool).await.expect("Migrations failed"); + + // Check if dummy data exists- a fake 'dummy_data' table is created if it does + let dummy_data_exists: bool = + sqlx::query_scalar("SELECT to_regclass('dummy_data') IS NOT NULL") + .fetch_one(&pool) + .await + .unwrap(); + if !dummy_data_exists { + // Add dummy data + let temporary_test_env = TestEnvironment::build_with_db(TemporaryDatabase { + pool: pool.clone(), + database_name: TEMPLATE_DATABASE_NAME.to_string(), + redis_pool: RedisPool::new(None), + }) + .await; + dummy_data::add_dummy_data(&temporary_test_env).await; + } + pool.close().await; + + // Switch back to main database (as we cant create from template while connected to it) + let pool = PgPool::connect(url.as_str()).await.unwrap(); + + // Create the temporary database from the template + let create_db_query = format!( + "CREATE DATABASE {} TEMPLATE {}", + &temp_database_name, TEMPLATE_DATABASE_NAME + ); + + sqlx::query(&create_db_query) + .execute(&pool) + .await + .expect("Database creation failed"); + + // Release the advisory lock + sqlx::query("SELECT pg_advisory_unlock(1)") + .execute(&main_pool) + .await + .unwrap(); + + main_pool.close().await; + break; + } + // Wait for the lock to be released + tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; + } + } + + // Deletes the temporary database (panics) // If a temporary db is created, it must be cleaned up with cleanup. // This means that dbs will only 'remain' if a test fails (for examination of the db), and will be cleaned up otherwise. pub async fn cleanup(mut self) { @@ -125,15 +216,9 @@ impl TemporaryDatabase { } } -fn generate_random_database_name() -> String { - // Generate a random database name here - // You can use your logic to create a unique name - // For example, you can use a random string as you did before - // or append a timestamp, etc. - - // We will use a random string starting with "labrinth_tests_db_" - // and append a 6-digit number to it. - let mut database_name = String::from("labrinth_tests_db_"); - database_name.push_str(&rand::random::().to_string()[..6]); - database_name +// Appends a random 8-digit number to the end of the str +pub fn generate_random_name(str: &str) -> String { + let mut str = String::from(str); + str.push_str(&rand::random::().to_string()[..8]); + str } diff --git a/tests/common/dummy_data.rs b/tests/common/dummy_data.rs index e66a88bc..7f7e10a4 100644 --- a/tests/common/dummy_data.rs +++ b/tests/common/dummy_data.rs @@ -1,5 +1,9 @@ +#![allow(dead_code)] use actix_web::test::{self, TestRequest}; -use labrinth::{models::projects::Project, models::projects::Version}; +use labrinth::{ + models::projects::Project, + models::{organizations::Organization, pats::Scopes, projects::Version}, +}; use serde_json::json; use sqlx::Executor; @@ -11,8 +15,10 @@ use super::{ request_data::get_public_project_creation_data, }; +pub const DUMMY_DATA_UPDATE: i64 = 1; + #[allow(dead_code)] -pub const DUMMY_CATEGORIES: &'static [&str] = &[ +pub const DUMMY_CATEGORIES: &[&str] = &[ "combat", "decoration", "economy", @@ -30,65 +36,145 @@ pub enum DummyJarFile { BasicModDifferent, } -pub struct DummyData { - pub alpha_team_id: String, - pub beta_team_id: String, - - pub alpha_project_id: String, - pub beta_project_id: String, +#[allow(dead_code)] +pub enum DummyImage { + SmallIcon, // 200x200 +} - pub alpha_project_slug: String, - pub beta_project_slug: String, +#[derive(Clone)] +pub struct DummyData { + pub project_alpha: DummyProjectAlpha, + pub project_beta: DummyProjectBeta, + pub organization_zeta: DummyOrganizationZeta, +} - pub alpha_version_id: String, - pub beta_version_id: String, +#[derive(Clone)] +pub struct DummyProjectAlpha { + // Alpha project: + // This is a dummy project created by USER user. + // It's approved, listed, and visible to the public. + pub project_id: String, + pub project_slug: String, + pub version_id: String, + pub thread_id: String, + pub file_hash: String, + pub team_id: String, +} - pub alpha_thread_id: String, - pub beta_thread_id: String, +#[derive(Clone)] +pub struct DummyProjectBeta { + // Beta project: + // This is a dummy project created by USER user. + // It's not approved, unlisted, and not visible to the public. + pub project_id: String, + pub project_slug: String, + pub version_id: String, + pub thread_id: String, + pub file_hash: String, + pub team_id: String, +} - pub alpha_file_hash: String, - pub beta_file_hash: String, +#[derive(Clone)] +pub struct DummyOrganizationZeta { + // Zeta organization: + // This is a dummy organization created by USER user. + // There are no projects in it. + pub organization_id: String, + pub organization_title: String, + pub team_id: String, } pub async fn add_dummy_data(test_env: &TestEnvironment) -> DummyData { // Adds basic dummy data to the database directly with sql (user, pats) let pool = &test_env.db.pool.clone(); - pool.execute(include_str!("../files/dummy_data.sql")) - .await - .unwrap(); + + pool.execute( + include_str!("../files/dummy_data.sql") + .replace("$1", &Scopes::all().bits().to_string()) + .as_str(), + ) + .await + .unwrap(); let (alpha_project, alpha_version) = add_project_alpha(test_env).await; let (beta_project, beta_version) = add_project_beta(test_env).await; + let zeta_organization = add_organization_zeta(test_env).await; + + sqlx::query("INSERT INTO dummy_data (update_id) VALUES ($1)") + .bind(DUMMY_DATA_UPDATE) + .execute(pool) + .await + .unwrap(); + DummyData { - alpha_team_id: alpha_project.team.to_string(), - beta_team_id: beta_project.team.to_string(), + project_alpha: DummyProjectAlpha { + team_id: alpha_project.team.to_string(), + project_id: alpha_project.id.to_string(), + project_slug: alpha_project.slug.unwrap(), + version_id: alpha_version.id.to_string(), + thread_id: alpha_project.thread_id.to_string(), + file_hash: alpha_version.files[0].hashes["sha1"].clone(), + }, - alpha_project_id: alpha_project.id.to_string(), - beta_project_id: beta_project.id.to_string(), + project_beta: DummyProjectBeta { + team_id: beta_project.team.to_string(), + project_id: beta_project.id.to_string(), + project_slug: beta_project.slug.unwrap(), + version_id: beta_version.id.to_string(), + thread_id: beta_project.thread_id.to_string(), + file_hash: beta_version.files[0].hashes["sha1"].clone(), + }, + + organization_zeta: DummyOrganizationZeta { + organization_id: zeta_organization.id.to_string(), + team_id: zeta_organization.team_id.to_string(), + organization_title: zeta_organization.title, + }, + } +} - alpha_project_slug: alpha_project.slug.unwrap(), - beta_project_slug: beta_project.slug.unwrap(), +pub async fn get_dummy_data(test_env: &TestEnvironment) -> DummyData { + let (alpha_project, alpha_version) = get_project_alpha(test_env).await; + let (beta_project, beta_version) = get_project_beta(test_env).await; - alpha_version_id: alpha_version.id.to_string(), - beta_version_id: beta_version.id.to_string(), + let zeta_organization = get_organization_zeta(test_env).await; + DummyData { + project_alpha: DummyProjectAlpha { + team_id: alpha_project.team.to_string(), + project_id: alpha_project.id.to_string(), + project_slug: alpha_project.slug.unwrap(), + version_id: alpha_version.id.to_string(), + thread_id: alpha_project.thread_id.to_string(), + file_hash: alpha_version.files[0].hashes["sha1"].clone(), + }, - alpha_thread_id: alpha_project.thread_id.to_string(), - beta_thread_id: beta_project.thread_id.to_string(), + project_beta: DummyProjectBeta { + team_id: beta_project.team.to_string(), + project_id: beta_project.id.to_string(), + project_slug: beta_project.slug.unwrap(), + version_id: beta_version.id.to_string(), + thread_id: beta_project.thread_id.to_string(), + file_hash: beta_version.files[0].hashes["sha1"].clone(), + }, - alpha_file_hash: alpha_version.files[0].hashes["sha1"].clone(), - beta_file_hash: beta_version.files[0].hashes["sha1"].clone(), + organization_zeta: DummyOrganizationZeta { + organization_id: zeta_organization.id.to_string(), + team_id: zeta_organization.team_id.to_string(), + organization_title: zeta_organization.title, + }, } } pub async fn add_project_alpha(test_env: &TestEnvironment) -> (Project, Version) { - test_env + let (project, versions) = test_env .v2 - .add_public_project(get_public_project_creation_data( - "alpha", - DummyJarFile::DummyProjectAlpha, - )) - .await + .add_public_project( + get_public_project_creation_data("alpha", Some(DummyJarFile::DummyProjectAlpha)), + USER_USER_PAT, + ) + .await; + (project, versions.into_iter().next().unwrap()) } pub async fn add_project_beta(test_env: &TestEnvironment) -> (Project, Version) { @@ -148,6 +234,48 @@ pub async fn add_project_beta(test_env: &TestEnvironment) -> (Project, Version) assert_eq!(resp.status(), 200); + get_project_beta(test_env).await +} + +pub async fn add_organization_zeta(test_env: &TestEnvironment) -> Organization { + // Add an organzation. + let req = TestRequest::post() + .uri("/v2/organization") + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "title": "zeta", + "description": "A dummy organization for testing with." + })) + .to_request(); + let resp = test_env.call(req).await; + + assert_eq!(resp.status(), 200); + + get_organization_zeta(test_env).await +} + +pub async fn get_project_alpha(test_env: &TestEnvironment) -> (Project, Version) { + // Get project + let req = TestRequest::get() + .uri("/v2/project/alpha") + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + let project: Project = test::read_body_json(resp).await; + + // Get project's versions + let req = TestRequest::get() + .uri("/v2/project/alpha/version") + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + let versions: Vec = test::read_body_json(resp).await; + let version = versions.into_iter().next().unwrap(); + + (project, version) +} + +pub async fn get_project_beta(test_env: &TestEnvironment) -> (Project, Version) { // Get project let req = TestRequest::get() .uri("/v2/project/beta") @@ -168,6 +296,18 @@ pub async fn add_project_beta(test_env: &TestEnvironment) -> (Project, Version) (project, version) } +pub async fn get_organization_zeta(test_env: &TestEnvironment) -> Organization { + // Get organization + let req = TestRequest::get() + .uri("/v2/organization/zeta") + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + let organization: Organization = test::read_body_json(resp).await; + + organization +} + impl DummyJarFile { pub fn filename(&self) -> String { match self { @@ -194,3 +334,25 @@ impl DummyJarFile { } } } + +impl DummyImage { + pub fn filename(&self) -> String { + match self { + DummyImage::SmallIcon => "200x200.png", + } + .to_string() + } + + pub fn extension(&self) -> String { + match self { + DummyImage::SmallIcon => "png", + } + .to_string() + } + + pub fn bytes(&self) -> Vec { + match self { + DummyImage::SmallIcon => include_bytes!("../../tests/files/200x200.png").to_vec(), + } + } +} diff --git a/tests/common/environment.rs b/tests/common/environment.rs index e3aa2ca9..abeaf730 100644 --- a/tests/common/environment.rs +++ b/tests/common/environment.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] -use std::rc::Rc; +use std::{rc::Rc, sync::Arc}; use super::{ api_v2::ApiV2, @@ -17,7 +17,7 @@ pub async fn with_test_environment(f: impl FnOnce(TestEnvironment) -> Fut) where Fut: Future, { - let test_env = TestEnvironment::build_with_dummy().await; + let test_env = TestEnvironment::build(None).await; let db = test_env.db.clone(); f(test_env).await; @@ -29,27 +29,29 @@ where // Must be called in an #[actix_rt::test] context. It also simulates a // temporary sqlx db like #[sqlx::test] would. // Use .call(req) on it directly to make a test call as if test::call_service(req) were being used. +#[derive(Clone)] pub struct TestEnvironment { - test_app: Rc>, + test_app: Rc, // Rc as it's not Send pub db: TemporaryDatabase, pub v2: ApiV2, - pub dummy: Option, + pub dummy: Option>, } impl TestEnvironment { - pub async fn build_with_dummy() -> Self { - let mut test_env = Self::build().await; - let dummy = dummy_data::add_dummy_data(&test_env).await; - test_env.dummy = Some(dummy); + pub async fn build(max_connections: Option) -> Self { + let db = TemporaryDatabase::create(max_connections).await; + let mut test_env = Self::build_with_db(db).await; + + let dummy = dummy_data::get_dummy_data(&test_env).await; + test_env.dummy = Some(Arc::new(dummy)); test_env } - pub async fn build() -> Self { - let db = TemporaryDatabase::create().await; + pub async fn build_with_db(db: TemporaryDatabase) -> Self { let labrinth_config = setup(&db).await; let app = App::new().configure(|cfg| labrinth::app_config(cfg, labrinth_config.clone())); - let test_app: Rc> = Rc::new(Box::new(test::init_service(app).await)); + let test_app: Rc = Rc::new(test::init_service(app).await); Self { v2: ApiV2 { test_app: test_app.clone(), @@ -59,6 +61,7 @@ impl TestEnvironment { dummy: None, } } + pub async fn cleanup(self) { self.db.cleanup().await; } @@ -71,8 +74,10 @@ impl TestEnvironment { let resp = self .v2 .add_user_to_team( - &self.dummy.as_ref().unwrap().alpha_team_id, + &self.dummy.as_ref().unwrap().project_alpha.team_id, FRIEND_USER_ID, + None, + None, USER_USER_PAT, ) .await; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index bf73c6fb..39b3305a 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -11,11 +11,12 @@ pub mod database; pub mod dummy_data; pub mod environment; pub mod pats; +pub mod permissions; pub mod request_data; pub mod scopes; // Testing equivalent to 'setup' function, producing a LabrinthConfig -// If making a test, you should probably use environment::TestEnvironment::build_with_dummy() (which calls this) +// If making a test, you should probably use environment::TestEnvironment::build() (which calls this) pub async fn setup(db: &TemporaryDatabase) -> LabrinthConfig { println!("Setting up labrinth config"); diff --git a/tests/common/permissions.rs b/tests/common/permissions.rs new file mode 100644 index 00000000..4ab33900 --- /dev/null +++ b/tests/common/permissions.rs @@ -0,0 +1,992 @@ +#![allow(dead_code)] +use actix_web::test::{self, TestRequest}; +use itertools::Itertools; +use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; +use serde_json::json; + +use crate::common::{ + database::{generate_random_name, ADMIN_USER_PAT}, + request_data, +}; + +use super::{ + database::{USER_USER_ID, USER_USER_PAT}, + environment::TestEnvironment, +}; + +// A reusable test type that works for any permissions test testing an endpoint that: +// - returns a known 'expected_failure_code' if the scope is not present (defaults to 401) +// - returns a 200-299 if the scope is present +// - returns failure and success JSON bodies for requests that are 200 (for performing non-simple follow-up tests on) +// This uses a builder format, so you can chain methods to set the parameters to non-defaults (most will probably be not need to be set). +pub struct PermissionsTest<'a> { + test_env: &'a TestEnvironment, + // Permissions expected to fail on this test. By default, this is all permissions except the success permissions. + // (To ensure we have isolated the permissions we are testing) + failure_project_permissions: Option, + failure_organization_permissions: Option, + + // User ID to use for the test user, and their PAT + user_id: &'a str, + user_pat: &'a str, + + // Whether or not the user ID should be removed from the project/organization team after the test + // (This is mostly reelvant if you are also using an existing project/organization, and want to do + // multiple tests with the same user. + remove_user: bool, + + // ID to use for the test project (project, organization) + // By default, create a new project or organization to test upon. + // However, if we want, we can use an existing project or organization. + // (eg: if we want to test a specific project, or a project with a specific state) + project_id: Option, + project_team_id: Option, + organization_id: Option, + organization_team_id: Option, + + // The codes that is allow to be returned if the scope is not present. + // (for instance, we might expect a 401, but not a 400) + allowed_failure_codes: Vec, +} + +pub struct PermissionsTestContext<'a> { + pub test_env: &'a TestEnvironment, + pub user_id: &'a str, + pub user_pat: &'a str, + pub project_id: Option<&'a str>, + pub team_id: Option<&'a str>, + pub organization_id: Option<&'a str>, + pub organization_team_id: Option<&'a str>, +} + +impl<'a> PermissionsTest<'a> { + pub fn new(test_env: &'a TestEnvironment) -> Self { + Self { + test_env, + failure_project_permissions: None, + failure_organization_permissions: None, + user_id: USER_USER_ID, + user_pat: USER_USER_PAT, + remove_user: false, + project_id: None, + organization_id: None, + project_team_id: None, + organization_team_id: None, + allowed_failure_codes: vec![401, 404], + } + } + + // Set non-standard failure permissions + // If not set, it will be set to all permissions except the success permissions + // (eg: if a combination of permissions is needed, but you want to make sure that the endpoint does not work with all-but-one of them) + pub fn with_failure_permissions( + mut self, + failure_project_permissions: Option, + failure_organization_permissions: Option, + ) -> Self { + self.failure_project_permissions = failure_project_permissions; + self.failure_organization_permissions = failure_organization_permissions; + self + } + + // Set the user ID to use + // (eg: a moderator, or friend) + // remove_user: Whether or not the user ID should be removed from the project/organization team after the test + pub fn with_user(mut self, user_id: &'a str, user_pat: &'a str, remove_user: bool) -> Self { + self.user_id = user_id; + self.user_pat = user_pat; + self.remove_user = remove_user; + self + } + + // If a non-standard code is expected. + // (eg: perhaps 200 for a resource with hidden values deeper in) + pub fn with_failure_codes( + mut self, + allowed_failure_codes: impl IntoIterator, + ) -> Self { + self.allowed_failure_codes = allowed_failure_codes.into_iter().collect(); + self + } + + // If an existing project or organization is intended to be used + // We will not create a new project, and will use the given project ID + // (But will still add the user to the project's team) + pub fn with_existing_project(mut self, project_id: &str, team_id: &str) -> Self { + self.project_id = Some(project_id.to_string()); + self.project_team_id = Some(team_id.to_string()); + self + } + pub fn with_existing_organization(mut self, organization_id: &str, team_id: &str) -> Self { + self.organization_id = Some(organization_id.to_string()); + self.organization_team_id = Some(team_id.to_string()); + self + } + + pub async fn simple_project_permissions_test( + &self, + success_permissions: ProjectPermissions, + req_gen: T, + ) -> Result<(), String> + where + T: Fn(&PermissionsTestContext) -> TestRequest, + { + let test_env = self.test_env; + let failure_project_permissions = self + .failure_project_permissions + .unwrap_or(ProjectPermissions::all() ^ success_permissions); + let test_context = PermissionsTestContext { + test_env, + user_id: self.user_id, + user_pat: self.user_pat, + project_id: None, + team_id: None, + organization_id: None, + organization_team_id: None, + }; + + let (project_id, team_id) = if self.project_id.is_some() && self.project_team_id.is_some() { + ( + self.project_id.clone().unwrap(), + self.project_team_id.clone().unwrap(), + ) + } else { + create_dummy_project(test_env).await + }; + + add_user_to_team( + self.user_id, + self.user_pat, + &team_id, + Some(failure_project_permissions), + None, + test_env, + ) + .await; + + // Failure test + let request = req_gen(&PermissionsTestContext { + project_id: Some(&project_id), + team_id: Some(&team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + + let resp = test_env.call(request).await; + if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { + return Err(format!( + "Failure permissions test failed. Expected failure codes {} got {}", + self.allowed_failure_codes + .iter() + .map(|code| code.to_string()) + .join(","), + resp.status().as_u16() + )); + } + + // Patch user's permissions to success permissions + modify_user_team_permissions( + self.user_id, + &team_id, + Some(success_permissions), + None, + test_env, + ) + .await; + + // Successful test + let request = req_gen(&PermissionsTestContext { + project_id: Some(&project_id), + team_id: Some(&team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + + let resp = test_env.call(request).await; + if !resp.status().is_success() { + return Err(format!( + "Success permissions test failed. Expected success, got {}", + resp.status().as_u16() + )); + } + + // If the remove_user flag is set, remove the user from the project + // Relevant for existing projects/users + if self.remove_user { + remove_user_from_team(self.user_id, &team_id, test_env).await; + } + Ok(()) + } + + pub async fn simple_organization_permissions_test( + &self, + success_permissions: OrganizationPermissions, + req_gen: T, + ) -> Result<(), String> + where + T: Fn(&PermissionsTestContext) -> TestRequest, + { + let test_env = self.test_env; + let failure_organization_permissions = self + .failure_organization_permissions + .unwrap_or(OrganizationPermissions::all() ^ success_permissions); + let test_context = PermissionsTestContext { + test_env, + user_id: self.user_id, + user_pat: self.user_pat, + project_id: None, + team_id: None, + organization_id: None, + organization_team_id: None, + }; + + let (organization_id, team_id) = + if self.organization_id.is_some() && self.organization_team_id.is_some() { + ( + self.organization_id.clone().unwrap(), + self.organization_team_id.clone().unwrap(), + ) + } else { + create_dummy_org(test_env).await + }; + + add_user_to_team( + self.user_id, + self.user_pat, + &team_id, + None, + Some(failure_organization_permissions), + test_env, + ) + .await; + + // Failure test + let request = req_gen(&PermissionsTestContext { + organization_id: Some(&organization_id), + team_id: Some(&team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + + let resp = test_env.call(request).await; + if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { + return Err(format!( + "Failure permissions test failed. Expected failure codes {} got {}", + self.allowed_failure_codes + .iter() + .map(|code| code.to_string()) + .join(","), + resp.status().as_u16() + )); + } + + // Patch user's permissions to success permissions + modify_user_team_permissions( + self.user_id, + &team_id, + None, + Some(success_permissions), + test_env, + ) + .await; + + // Successful test + let request = req_gen(&PermissionsTestContext { + organization_id: Some(&organization_id), + team_id: Some(&team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + + let resp = test_env.call(request).await; + if !resp.status().is_success() { + return Err(format!( + "Success permissions test failed. Expected success, got {}", + resp.status().as_u16() + )); + } + + // If the remove_user flag is set, remove the user from the organization + // Relevant for existing projects/users + if self.remove_user { + remove_user_from_team(self.user_id, &team_id, test_env).await; + } + Ok(()) + } + + pub async fn full_project_permissions_test( + &self, + success_permissions: ProjectPermissions, + req_gen: T, + ) -> Result<(), String> + where + T: Fn(&PermissionsTestContext) -> TestRequest, + { + let test_env = self.test_env; + let failure_project_permissions = self + .failure_project_permissions + .unwrap_or(ProjectPermissions::all() ^ success_permissions); + let test_context = PermissionsTestContext { + test_env, + user_id: self.user_id, + user_pat: self.user_pat, + project_id: None, + team_id: None, + organization_id: None, + organization_team_id: None, + }; + + // TEST 1: Failure + // Random user, unaffiliated with the project, with no permissions + let test_1 = async { + let (project_id, team_id) = create_dummy_project(test_env).await; + + let request = req_gen(&PermissionsTestContext { + project_id: Some(&project_id), + team_id: Some(&team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + let resp = test_env.call(request).await; + if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { + return Err(format!( + "Test 1 failed. Expected failure codes {} got {}", + self.allowed_failure_codes + .iter() + .map(|code| code.to_string()) + .join(","), + resp.status().as_u16() + )); + } + + let p = + get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + if p != ProjectPermissions::empty() { + return Err(format!( + "Test 1 failed. Expected no permissions, got {:?}", + p + )); + } + + Ok(()) + }; + + // TEST 2: Failure + // User affiliated with the project, with failure permissions + let test_2 = async { + let (project_id, team_id) = create_dummy_project(test_env).await; + add_user_to_team( + self.user_id, + self.user_pat, + &team_id, + Some(failure_project_permissions), + None, + test_env, + ) + .await; + + let request = req_gen(&PermissionsTestContext { + project_id: Some(&project_id), + team_id: Some(&team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + + let resp = test_env.call(request).await; + if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { + return Err(format!( + "Test 2 failed. Expected failure codes {} got {}", + self.allowed_failure_codes + .iter() + .map(|code| code.to_string()) + .join(","), + resp.status().as_u16() + )); + } + + let p = + get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + if p != failure_project_permissions { + return Err(format!( + "Test 2 failed. Expected {:?}, got {:?}", + failure_project_permissions, p + )); + } + + Ok(()) + }; + + // TEST 3: Success + // User affiliated with the project, with the given permissions + let test_3 = async { + let (project_id, team_id) = create_dummy_project(test_env).await; + add_user_to_team( + self.user_id, + self.user_pat, + &team_id, + Some(success_permissions), + None, + test_env, + ) + .await; + + let request = req_gen(&PermissionsTestContext { + project_id: Some(&project_id), + team_id: Some(&team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + + let resp = test_env.call(request).await; + if !resp.status().is_success() { + return Err(format!( + "Test 3 failed. Expected success, got {}", + resp.status().as_u16() + )); + } + + let p = + get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + if p != success_permissions { + return Err(format!( + "Test 3 failed. Expected {:?}, got {:?}", + success_permissions, p + )); + } + + Ok(()) + }; + + // TEST 4: Failure + // Project has an organization + // User affiliated with the project's org, with default failure permissions + let test_4 = async { + let (project_id, team_id) = create_dummy_project(test_env).await; + let (organization_id, organization_team_id) = create_dummy_org(test_env).await; + add_project_to_org(test_env, &project_id, &organization_id).await; + add_user_to_team( + self.user_id, + self.user_pat, + &organization_team_id, + Some(failure_project_permissions), + None, + test_env, + ) + .await; + + let request = req_gen(&PermissionsTestContext { + project_id: Some(&project_id), + team_id: Some(&team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + + let resp = test_env.call(request).await; + if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { + return Err(format!( + "Test 4 failed. Expected failure codes {} got {}", + self.allowed_failure_codes + .iter() + .map(|code| code.to_string()) + .join(","), + resp.status().as_u16() + )); + } + + let p = + get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + if p != failure_project_permissions { + return Err(format!( + "Test 4 failed. Expected {:?}, got {:?}", + failure_project_permissions, p + )); + } + + Ok(()) + }; + + // TEST 5: Success + // Project has an organization + // User affiliated with the project's org, with the default success + let test_5 = async { + let (project_id, team_id) = create_dummy_project(test_env).await; + let (organization_id, organization_team_id) = create_dummy_org(test_env).await; + add_project_to_org(test_env, &project_id, &organization_id).await; + add_user_to_team( + self.user_id, + self.user_pat, + &organization_team_id, + Some(success_permissions), + None, + test_env, + ) + .await; + + let request = req_gen(&PermissionsTestContext { + project_id: Some(&project_id), + team_id: Some(&team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + + let resp = test_env.call(request).await; + if !resp.status().is_success() { + return Err(format!( + "Test 5 failed. Expected success, got {}", + resp.status().as_u16() + )); + } + + let p = + get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + if p != success_permissions { + return Err(format!( + "Test 5 failed. Expected {:?}, got {:?}", + success_permissions, p + )); + } + + Ok(()) + }; + + // TEST 6: Failure + // Project has an organization + // User affiliated with the project's org (even can have successful permissions!) + // User overwritten on the project team with failure permissions + let test_6 = async { + let (project_id, team_id) = create_dummy_project(test_env).await; + let (organization_id, organization_team_id) = create_dummy_org(test_env).await; + add_project_to_org(test_env, &project_id, &organization_id).await; + add_user_to_team( + self.user_id, + self.user_pat, + &organization_team_id, + Some(success_permissions), + None, + test_env, + ) + .await; + add_user_to_team( + self.user_id, + self.user_pat, + &team_id, + Some(failure_project_permissions), + None, + test_env, + ) + .await; + + let request = req_gen(&PermissionsTestContext { + project_id: Some(&project_id), + team_id: Some(&team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + + let resp = test_env.call(request).await; + if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { + return Err(format!( + "Test 6 failed. Expected failure codes {} got {}", + self.allowed_failure_codes + .iter() + .map(|code| code.to_string()) + .join(","), + resp.status().as_u16() + )); + } + + let p = + get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + if p != failure_project_permissions { + return Err(format!( + "Test 6 failed. Expected {:?}, got {:?}", + failure_project_permissions, p + )); + } + + Ok(()) + }; + + // TEST 7: Success + // Project has an organization + // User affiliated with the project's org with default failure permissions + // User overwritten to the project with the success permissions + let test_7 = async { + let (project_id, team_id) = create_dummy_project(test_env).await; + let (organization_id, organization_team_id) = create_dummy_org(test_env).await; + add_project_to_org(test_env, &project_id, &organization_id).await; + add_user_to_team( + self.user_id, + self.user_pat, + &organization_team_id, + Some(failure_project_permissions), + None, + test_env, + ) + .await; + add_user_to_team( + self.user_id, + self.user_pat, + &team_id, + Some(success_permissions), + None, + test_env, + ) + .await; + + let request = req_gen(&PermissionsTestContext { + project_id: Some(&project_id), + team_id: Some(&team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + + let resp = test_env.call(request).await; + + if !resp.status().is_success() { + return Err(format!( + "Test 7 failed. Expected success, got {}", + resp.status().as_u16() + )); + } + + let p = + get_project_permissions(self.user_id, self.user_pat, &project_id, test_env).await; + if p != success_permissions { + return Err(format!( + "Test 7 failed. Expected {:?}, got {:?}", + success_permissions, p + )); + } + + Ok(()) + }; + + tokio::try_join!(test_1, test_2, test_3, test_4, test_5, test_6, test_7,) + .map_err(|e| e.to_string())?; + + Ok(()) + } + + pub async fn full_organization_permissions_tests( + &self, + success_permissions: OrganizationPermissions, + req_gen: T, + ) -> Result<(), String> + where + T: Fn(&PermissionsTestContext) -> TestRequest, + { + let test_env = self.test_env; + let failure_organization_permissions = self + .failure_organization_permissions + .unwrap_or(OrganizationPermissions::all() ^ success_permissions); + let test_context = PermissionsTestContext { + test_env, + user_id: self.user_id, + user_pat: self.user_pat, + project_id: None, // Will be overwritten on each test + team_id: None, // Will be overwritten on each test + organization_id: None, + organization_team_id: None, + }; + + // TEST 1: Failure + // Random user, entirely unaffliaited with the organization + let test_1 = async { + let (organization_id, organization_team_id) = create_dummy_org(test_env).await; + + let request = req_gen(&PermissionsTestContext { + organization_id: Some(&organization_id), + organization_team_id: Some(&organization_team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + let resp = test_env.call(request).await; + if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { + return Err(format!( + "Test 1 failed. Expected failure codes {} got {}", + self.allowed_failure_codes + .iter() + .map(|code| code.to_string()) + .join(","), + resp.status().as_u16() + )); + } + + let p = get_organization_permissions( + self.user_id, + self.user_pat, + &organization_id, + test_env, + ) + .await; + if p != OrganizationPermissions::empty() { + return Err(format!( + "Test 1 failed. Expected no permissions, got {:?}", + p + )); + } + Ok(()) + }; + + // TEST 2: Failure + // User affiliated with the organization, with failure permissions + let test_2 = async { + let (organization_id, organization_team_id) = create_dummy_org(test_env).await; + add_user_to_team( + self.user_id, + self.user_pat, + &organization_team_id, + None, + Some(failure_organization_permissions), + test_env, + ) + .await; + + let request = req_gen(&PermissionsTestContext { + organization_id: Some(&organization_id), + organization_team_id: Some(&organization_team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + + let resp = test_env.call(request).await; + if !self.allowed_failure_codes.contains(&resp.status().as_u16()) { + return Err(format!( + "Test 2 failed. Expected failure codes {} got {}", + self.allowed_failure_codes + .iter() + .map(|code| code.to_string()) + .join(","), + resp.status().as_u16() + )); + } + + let p = get_organization_permissions( + self.user_id, + self.user_pat, + &organization_id, + test_env, + ) + .await; + if p != failure_organization_permissions { + return Err(format!( + "Test 2 failed. Expected {:?}, got {:?}", + failure_organization_permissions, p + )); + } + Ok(()) + }; + + // TEST 3: Success + // User affiliated with the organization, with the given permissions + let test_3 = async { + let (organization_id, organization_team_id) = create_dummy_org(test_env).await; + add_user_to_team( + self.user_id, + self.user_pat, + &organization_team_id, + None, + Some(success_permissions), + test_env, + ) + .await; + + let request = req_gen(&PermissionsTestContext { + organization_id: Some(&organization_id), + organization_team_id: Some(&organization_team_id), + ..test_context + }) + .append_header(("Authorization", self.user_pat)) + .to_request(); + + let resp = test_env.call(request).await; + if !resp.status().is_success() { + return Err(format!( + "Test 3 failed. Expected success, got {}", + resp.status().as_u16() + )); + } + + let p = get_organization_permissions( + self.user_id, + self.user_pat, + &organization_id, + test_env, + ) + .await; + if p != success_permissions { + return Err(format!( + "Test 3 failed. Expected {:?}, got {:?}", + success_permissions, p + )); + } + Ok(()) + }; + + tokio::try_join!(test_1, test_2, test_3,).map_err(|e| e.to_string())?; + + Ok(()) + } +} + +async fn create_dummy_project(test_env: &TestEnvironment) -> (String, String) { + let api = &test_env.v2; + + // Create a very simple project + let slug = generate_random_name("test_project"); + + let creation_data = request_data::get_public_project_creation_data(&slug, None); + let (project, _) = api.add_public_project(creation_data, ADMIN_USER_PAT).await; + let project_id = project.id.to_string(); + let team_id = project.team.to_string(); + + (project_id, team_id) +} + +async fn create_dummy_org(test_env: &TestEnvironment) -> (String, String) { + // Create a very simple organization + let name = generate_random_name("test_org"); + let api = &test_env.v2; + + let resp = api + .create_organization(&name, "Example description.", ADMIN_USER_PAT) + .await; + assert!(resp.status().is_success()); + + let organization = api + .get_organization_deserialized(&name, ADMIN_USER_PAT) + .await; + let organizaion_id = organization.id.to_string(); + let team_id = organization.team_id.to_string(); + + (organizaion_id, team_id) +} + +async fn add_project_to_org(test_env: &TestEnvironment, project_id: &str, organization_id: &str) { + let api = &test_env.v2; + let resp = api + .organization_add_project(organization_id, project_id, ADMIN_USER_PAT) + .await; + assert!(resp.status().is_success()); +} + +async fn add_user_to_team( + user_id: &str, + user_pat: &str, + team_id: &str, + project_permissions: Option, + organization_permissions: Option, + test_env: &TestEnvironment, +) { + let api = &test_env.v2; + + // Invite user + let resp = api + .add_user_to_team( + team_id, + user_id, + project_permissions, + organization_permissions, + ADMIN_USER_PAT, + ) + .await; + assert!(resp.status().is_success()); + + // Accept invitation + let resp = api.join_team(team_id, user_pat).await; + assert!(resp.status().is_success()); +} + +async fn modify_user_team_permissions( + user_id: &str, + team_id: &str, + permissions: Option, + organization_permissions: Option, + test_env: &TestEnvironment, +) { + let api = &test_env.v2; + + // Send invitation to user + let resp = api + .edit_team_member( + team_id, + user_id, + json!({ + "permissions" : permissions.map(|p| p.bits()), + "organization_permissions" : organization_permissions.map(|p| p.bits()), + }), + ADMIN_USER_PAT, + ) + .await; + assert!(resp.status().is_success()); +} + +async fn remove_user_from_team(user_id: &str, team_id: &str, test_env: &TestEnvironment) { + // Send invitation to user + let api = &test_env.v2; + let resp = api.remove_from_team(team_id, user_id, ADMIN_USER_PAT).await; + assert!(resp.status().is_success()); +} + +async fn get_project_permissions( + user_id: &str, + user_pat: &str, + project_id: &str, + test_env: &TestEnvironment, +) -> ProjectPermissions { + let resp = test_env.v2.get_project_members(project_id, user_pat).await; + let permissions = if resp.status().as_u16() == 200 { + let value: serde_json::Value = test::read_body_json(resp).await; + value + .as_array() + .unwrap() + .iter() + .find(|member| member["user"]["id"].as_str().unwrap() == user_id) + .map(|member| member["permissions"].as_u64().unwrap()) + .unwrap_or_default() + } else { + 0 + }; + + ProjectPermissions::from_bits_truncate(permissions) +} + +async fn get_organization_permissions( + user_id: &str, + user_pat: &str, + organization_id: &str, + test_env: &TestEnvironment, +) -> OrganizationPermissions { + let api = &test_env.v2; + let resp = api + .get_organization_members(organization_id, user_pat) + .await; + let permissions = if resp.status().as_u16() == 200 { + let value: serde_json::Value = test::read_body_json(resp).await; + value + .as_array() + .unwrap() + .iter() + .find(|member| member["user"]["id"].as_str().unwrap() == user_id) + .map(|member| member["organization_permissions"].as_u64().unwrap()) + .unwrap_or_default() + } else { + 0 + }; + + OrganizationPermissions::from_bits_truncate(permissions) +} diff --git a/tests/common/request_data.rs b/tests/common/request_data.rs index 85dce64b..bd5eb284 100644 --- a/tests/common/request_data.rs +++ b/tests/common/request_data.rs @@ -1,18 +1,45 @@ +#![allow(dead_code)] use serde_json::json; -use super::{actix::MultipartSegment, dummy_data::DummyJarFile}; +use super::{ + actix::MultipartSegment, + dummy_data::{DummyImage, DummyJarFile}, +}; use crate::common::actix::MultipartSegmentData; pub struct ProjectCreationRequestData { pub slug: String, - pub jar: DummyJarFile, + pub jar: Option, pub segment_data: Vec, } +pub struct ImageData { + pub filename: String, + pub extension: String, + pub icon: Vec, +} + pub fn get_public_project_creation_data( slug: &str, - jar: DummyJarFile, + version_jar: Option, ) -> ProjectCreationRequestData { + let initial_versions = if let Some(ref jar) = version_jar { + json!([{ + "file_parts": [jar.filename()], + "version_number": "1.2.3", + "version_title": "start", + "dependencies": [], + "game_versions": ["1.20.1"] , + "release_channel": "release", + "loaders": ["fabric"], + "featured": true + }]) + } else { + json!([]) + }; + + let is_draft = version_jar.is_none(); + let json_data = json!( { "title": format!("Test Project {slug}"), @@ -21,16 +48,8 @@ pub fn get_public_project_creation_data( "body": "This project is approved, and versions are listed.", "client_side": "required", "server_side": "optional", - "initial_versions": [{ - "file_parts": [jar.filename()], - "version_number": "1.2.3", - "version_title": "start", - "dependencies": [], - "game_versions": ["1.20.1"] , - "release_channel": "release", - "loaders": ["fabric"], - "featured": true - }], + "initial_versions": initial_versions, + "is_draft": is_draft, "categories": [], "license_id": "MIT" } @@ -44,17 +63,31 @@ pub fn get_public_project_creation_data( data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), }; - // Basic file - let file_segment = MultipartSegment { - name: jar.filename(), - filename: Some(jar.filename()), - content_type: Some("application/java-archive".to_string()), - data: MultipartSegmentData::Binary(jar.bytes()), + let segment_data = if let Some(ref jar) = version_jar { + // Basic file + let file_segment = MultipartSegment { + name: jar.filename(), + filename: Some(jar.filename()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary(jar.bytes()), + }; + + vec![json_segment.clone(), file_segment] + } else { + vec![json_segment.clone()] }; ProjectCreationRequestData { slug: slug.to_string(), - jar, - segment_data: vec![json_segment.clone(), file_segment.clone()], + jar: version_jar, + segment_data, + } +} + +pub fn get_icon_data(dummy_icon: DummyImage) -> ImageData { + ImageData { + filename: dummy_icon.filename(), + extension: dummy_icon.extension(), + icon: dummy_icon.bytes(), } } diff --git a/tests/files/dummy_data.sql b/tests/files/dummy_data.sql index 0583f8af..487397a5 100644 --- a/tests/files/dummy_data.sql +++ b/tests/files/dummy_data.sql @@ -13,11 +13,11 @@ INSERT INTO users (id, username, name, email, role) VALUES (5, 'enemy', 'Enemy T -- Full PATs for each user, with different scopes -- These are not legal PATs, as they contain all scopes- they mimic permissions of a logged in user -- IDs: 50-54, o p q r s -INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (50, 1, 'admin-pat', 'mrp_patadmin', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00'); -INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (51, 2, 'moderator-pat', 'mrp_patmoderator', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00'); -INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (52, 3, 'user-pat', 'mrp_patuser', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00'); -INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (53, 4, 'friend-pat', 'mrp_patfriend', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00'); -INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (54, 5, 'enemy-pat', 'mrp_patenemy', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00'); +INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (50, 1, 'admin-pat', 'mrp_patadmin', $1, '2030-08-18 15:48:58.435729+00'); +INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (51, 2, 'moderator-pat', 'mrp_patmoderator', $1, '2030-08-18 15:48:58.435729+00'); +INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (52, 3, 'user-pat', 'mrp_patuser', $1, '2030-08-18 15:48:58.435729+00'); +INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (53, 4, 'friend-pat', 'mrp_patfriend', $1, '2030-08-18 15:48:58.435729+00'); +INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (54, 5, 'enemy-pat', 'mrp_patenemy', $1, '2030-08-18 15:48:58.435729+00'); -- -- Sample game versions, loaders, categories INSERT INTO game_versions (id, version, type, created) @@ -43,4 +43,9 @@ INSERT INTO categories (id, category, project_type) VALUES (104, 'food', 2), (105, 'magic', 2), (106, 'mobs', 2), - (107, 'optimization', 2); \ No newline at end of file + (107, 'optimization', 2); + +-- Create dummy data table to mark that this file has been run +CREATE TABLE dummy_data ( + update_id bigint PRIMARY KEY + ); diff --git a/tests/notifications.rs b/tests/notifications.rs index b61c58de..29dd4b7d 100644 --- a/tests/notifications.rs +++ b/tests/notifications.rs @@ -8,12 +8,18 @@ mod common; #[actix_rt::test] pub async fn get_user_notifications_after_team_invitation_returns_notification() { with_test_environment(|test_env| async move { - let alpha_team_id = test_env.dummy.as_ref().unwrap().alpha_team_id.clone(); + let alpha_team_id = test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .team_id + .clone(); let api = test_env.v2; api.get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) .await; - api.add_user_to_team(&alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) + api.add_user_to_team(&alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) .await; let notifications = api diff --git a/tests/organizations.rs b/tests/organizations.rs new file mode 100644 index 00000000..edbb84bd --- /dev/null +++ b/tests/organizations.rs @@ -0,0 +1,649 @@ +use crate::common::{ + database::{generate_random_name, ADMIN_USER_PAT, MOD_USER_ID, MOD_USER_PAT, USER_USER_ID}, + dummy_data::DummyImage, + environment::TestEnvironment, + request_data::get_icon_data, +}; +use actix_web::test; +use bytes::Bytes; +use common::{ + database::{FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_PAT}, + permissions::{PermissionsTest, PermissionsTestContext}, +}; +use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; +use serde_json::json; + +mod common; + +#[actix_rt::test] +async fn create_organization() { + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; + let zeta_organization_slug = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + + // Failed creations title: + // - slug collision with zeta + // - too short slug + // - too long slug + // - not url safe slug + for title in [ + zeta_organization_slug, + "a", + &"a".repeat(100), + "not url safe%&^!#$##!@#$%^&*()", + ] { + let resp = api + .create_organization(title, "theta_description", USER_USER_PAT) + .await; + assert_eq!(resp.status(), 400); + } + + // Failed creations description: + // - too short slug + // - too long slug + for description in ["a", &"a".repeat(300)] { + let resp = api + .create_organization("theta", description, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 400); + } + + // Create 'theta' organization + let resp = api + .create_organization("theta", "not url safe%&^!#$##!@#$%^&", USER_USER_PAT) + .await; + assert_eq!(resp.status(), 200); + + // Get organization using slug + let theta = api + .get_organization_deserialized("theta", USER_USER_PAT) + .await; + assert_eq!(theta.title, "theta"); + assert_eq!(theta.description, "not url safe%&^!#$##!@#$%^&"); + assert_eq!(resp.status(), 200); + + // Get created team + let members = api + .get_organization_members_deserialized("theta", USER_USER_PAT) + .await; + + // Should only be one member, which is USER_USER_ID, and is the owner with full permissions + assert_eq!(members[0].user.id.to_string(), USER_USER_ID); + assert_eq!( + members[0].organization_permissions, + Some(OrganizationPermissions::all()) + ); + assert_eq!(members[0].role, "Owner"); + + test_env.cleanup().await; +} + +#[actix_rt::test] +async fn patch_organization() { + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; + + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + + // Create 'theta' organization + let resp = api + .create_organization("theta", "theta_description", USER_USER_PAT) + .await; + assert_eq!(resp.status(), 200); + + // Failed patch to zeta slug: + // - slug collision with theta + // - too short slug + // - too long slug + // - not url safe slug + for title in [ + "theta", + "a", + &"a".repeat(100), + "not url safe%&^!#$##!@#$%^&*()", + ] { + let resp = api + .edit_organization( + zeta_organization_id, + json!({ + "title": title, + "description": "theta_description" + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Failed patch to zeta description: + // - too short description + // - too long description + for description in ["a", &"a".repeat(300)] { + let resp = api + .edit_organization( + zeta_organization_id, + json!({ + "description": description + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 400); + } + + // Successful patch to many fields + let resp = api + .edit_organization( + zeta_organization_id, + json!({ + "title": "new_title", + "description": "not url safe%&^!#$##!@#$%^&" // not-URL-safe description should still work + }), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + + // Get project using new slug + let new_title = api + .get_organization_deserialized("new_title", USER_USER_PAT) + .await; + assert_eq!(new_title.title, "new_title"); + assert_eq!(new_title.description, "not url safe%&^!#$##!@#$%^&"); + + test_env.cleanup().await; +} + +// add/remove icon +#[actix_rt::test] +async fn add_remove_icon() { + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + + // Get project + let resp = test_env + .v2 + .get_organization_deserialized(zeta_organization_id, USER_USER_PAT) + .await; + assert_eq!(resp.icon_url, None); + + // Icon edit + // Uses alpha organization to delete this icon + let resp = api + .edit_organization_icon( + zeta_organization_id, + Some(get_icon_data(DummyImage::SmallIcon)), + USER_USER_PAT, + ) + .await; + assert_eq!(resp.status(), 204); + + // Get project + let zeta_org = api + .get_organization_deserialized(zeta_organization_id, USER_USER_PAT) + .await; + assert!(zeta_org.icon_url.is_some()); + + // Icon delete + // Uses alpha organization to delete added icon + let resp = api + .edit_organization_icon(zeta_organization_id, None, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 204); + + // Get project + let zeta_org = api + .get_organization_deserialized(zeta_organization_id, USER_USER_PAT) + .await; + assert!(zeta_org.icon_url.is_none()); + + test_env.cleanup().await; +} + +// delete org +#[actix_rt::test] +async fn delete_org() { + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + + let resp = api + .delete_organization(zeta_organization_id, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 204); + + // Get organization, which should no longer exist + let resp = api + .get_organization(zeta_organization_id, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 404); + + test_env.cleanup().await; +} + +// add/remove organization projects +#[actix_rt::test] +async fn add_remove_organization_projects() { + let test_env = TestEnvironment::build(None).await; + let alpha_project_id: &str = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; + let alpha_project_slug: &str = &test_env.dummy.as_ref().unwrap().project_alpha.project_slug; + let zeta_organization_id: &str = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + + // Add/remove project to organization, first by ID, then by slug + for alpha in [alpha_project_id, alpha_project_slug] { + let resp = test_env + .v2 + .organization_add_project(zeta_organization_id, alpha, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 200); + + // Get organization projects + let projects = test_env + .v2 + .get_organization_projects_deserialized(zeta_organization_id, USER_USER_PAT) + .await; + assert_eq!(projects[0].id.to_string(), alpha_project_id); + assert_eq!(projects[0].slug, Some(alpha_project_slug.to_string())); + + // Remove project from organization + let resp = test_env + .v2 + .organization_remove_project(zeta_organization_id, alpha, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 200); + + // Get organization projects + let projects = test_env + .v2 + .get_organization_projects_deserialized(zeta_organization_id, USER_USER_PAT) + .await; + assert!(projects.is_empty()); + } + + test_env.cleanup().await; +} + +#[actix_rt::test] +async fn permissions_patch_organization() { + let test_env = TestEnvironment::build(Some(8)).await; + + // For each permission covered by EDIT_DETAILS, ensure the permission is required + let edit_details = OrganizationPermissions::EDIT_DETAILS; + let test_pairs = [ + ("title", json!("")), // generated in the test to not collide slugs + ("description", json!("New description")), + ]; + + for (key, value) in test_pairs { + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!( + "/v2/organization/{}", + ctx.organization_id.unwrap() + )) + .set_json(json!({ + key: if key == "title" { + json!(generate_random_name("randomslug")) + } else { + value.clone() + }, + })) + }; + PermissionsTest::new(&test_env) + .simple_organization_permissions_test(edit_details, req_gen) + .await + .unwrap(); + } + + test_env.cleanup().await; +} + +// Not covered by PATCH /organization +#[actix_rt::test] +async fn permissions_edit_details() { + let test_env = TestEnvironment::build(None).await; + + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + let edit_details = OrganizationPermissions::EDIT_DETAILS; + + // Icon edit + // Uses alpha organization to delete this icon + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!( + "/v2/organization/{}/icon?ext=png", + ctx.organization_id.unwrap() + )) + .set_payload(Bytes::from( + include_bytes!("../tests/files/200x200.png") as &[u8] + )) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_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!( + "/v2/organization/{}/icon?ext=png", + ctx.organization_id.unwrap() + )) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(edit_details, req_gen) + .await + .unwrap(); +} + +#[actix_rt::test] +async fn permissions_manage_invites() { + // Add member, remove member, edit member + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; + + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + let manage_invites = OrganizationPermissions::MANAGE_INVITES; + + // Add member + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::post() + .uri(&format!("/v2/team/{}/members", ctx.team_id.unwrap())) + .set_json(json!({ + "user_id": MOD_USER_ID, + "permissions": 0, + "organization_permissions": 0, + })) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(manage_invites, req_gen) + .await + .unwrap(); + + // Edit member + let edit_member = OrganizationPermissions::EDIT_MEMBER; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!( + "/v2/team/{}/members/{MOD_USER_ID}", + ctx.team_id.unwrap() + )) + .set_json(json!({ + "organization_permissions": 0, + })) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_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!( + "/v2/team/{}/members/{MOD_USER_ID}", + ctx.team_id.unwrap() + )) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(manage_invites, req_gen) + .await + .unwrap(); + + // re-add member for testing + let resp = api + .add_user_to_team(zeta_team_id, MOD_USER_ID, None, None, ADMIN_USER_PAT) + .await; + assert_eq!(resp.status(), 204); + let resp = api.join_team(zeta_team_id, MOD_USER_PAT).await; + assert_eq!(resp.status(), 204); + + // remove existing member (requires remove_member) + let remove_member = OrganizationPermissions::REMOVE_MEMBER; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( + "/v2/team/{}/members/{MOD_USER_ID}", + ctx.team_id.unwrap() + )) + }; + + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(remove_member, req_gen) + .await + .unwrap(); + + test_env.cleanup().await; +} + +#[actix_rt::test] +async fn permissions_add_remove_project() { + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; + + 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 zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + let add_project = OrganizationPermissions::ADD_PROJECT; + + // First, we add FRIEND_USER_ID to the alpha project and transfer ownership to them + // This is because the ownership of a project is needed to add it to an organization + let resp = api + .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 204); + let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await; + assert_eq!(resp.status(), 204); + let resp = api + .transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) + .await; + assert_eq!(resp.status(), 204); + + // Now, FRIEND_USER_ID owns the alpha project + // Add alpha project to zeta organization + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::post() + .uri(&format!( + "/v2/organization/{}/projects", + ctx.organization_id.unwrap() + )) + .set_json(json!({ + "project_id": alpha_project_id, + })) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(add_project, req_gen) + .await + .unwrap(); + + // Remove alpha project from zeta organization + let remove_project = OrganizationPermissions::REMOVE_PROJECT; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( + "/v2/organization/{}/projects/{alpha_project_id}", + ctx.organization_id.unwrap() + )) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .simple_organization_permissions_test(remove_project, req_gen) + .await + .unwrap(); + + test_env.cleanup().await; +} + +#[actix_rt::test] +async fn permissions_delete_organization() { + let test_env = TestEnvironment::build(None).await; + let delete_organization = OrganizationPermissions::DELETE_ORGANIZATION; + + // Now, FRIEND_USER_ID owns the alpha project + // Add alpha project to zeta organization + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( + "/v2/organization/{}", + ctx.organization_id.unwrap() + )) + }; + PermissionsTest::new(&test_env) + .simple_organization_permissions_test(delete_organization, req_gen) + .await + .unwrap(); + + test_env.cleanup().await; +} + +#[actix_rt::test] +async fn permissions_add_default_project_permissions() { + let test_env = TestEnvironment::build(None).await; + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + // Add member + let add_member_default_permissions = OrganizationPermissions::MANAGE_INVITES + | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS; + + // Failure test should include MANAGE_INVITES, as it is required to add + // default permissions on an invited user, but should still fail without EDIT_MEMBER_DEFAULT_PERMISSIONS + let failure_with_add_member = (OrganizationPermissions::all() ^ add_member_default_permissions) + | OrganizationPermissions::MANAGE_INVITES; + + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::post() + .uri(&format!("/v2/team/{}/members", ctx.team_id.unwrap())) + .set_json(json!({ + "user_id": MOD_USER_ID, + // do not set permissions as it will be set to default, which is non-zero + "organization_permissions": 0, + })) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .with_failure_permissions(None, Some(failure_with_add_member)) + .simple_organization_permissions_test(add_member_default_permissions, req_gen) + .await + .unwrap(); + + // Now that member is added, modify default permissions + let modify_member_default_permission = OrganizationPermissions::EDIT_MEMBER + | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS; + + // Failure test should include MANAGE_INVITES, as it is required to add + // default permissions on an invited user, but should still fail without EDIT_MEMBER_DEFAULT_PERMISSIONS + let failure_with_modify_member = (OrganizationPermissions::all() + ^ add_member_default_permissions) + | OrganizationPermissions::EDIT_MEMBER; + + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!( + "/v2/team/{}/members/{MOD_USER_ID}", + ctx.team_id.unwrap() + )) + .set_json(json!({ + "permissions": ProjectPermissions::EDIT_DETAILS.bits(), + })) + }; + PermissionsTest::new(&test_env) + .with_existing_organization(zeta_organization_id, zeta_team_id) + .with_user(FRIEND_USER_ID, FRIEND_USER_PAT, true) + .with_failure_permissions(None, Some(failure_with_modify_member)) + .simple_organization_permissions_test(modify_member_default_permission, req_gen) + .await + .unwrap(); + + test_env.cleanup().await; +} + +#[actix_rt::test] +async fn permissions_organization_permissions_consistency_test() { + let test_env = TestEnvironment::build(None).await; + // Ensuring that permission are as we expect them to be + // Full organization permissions test + let success_permissions = OrganizationPermissions::EDIT_DETAILS; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!( + "/v2/organization/{}", + ctx.organization_id.unwrap() + )) + .set_json(json!({ + "description": "Example description - changed.", + })) + }; + PermissionsTest::new(&test_env) + .full_organization_permissions_tests(success_permissions, req_gen) + .await + .unwrap(); + + test_env.cleanup().await; +} diff --git a/tests/pats.rs b/tests/pats.rs index 98da30ec..fe60639e 100644 --- a/tests/pats.rs +++ b/tests/pats.rs @@ -18,7 +18,7 @@ mod common; // - ensure PATs can be deleted #[actix_rt::test] pub async fn pat_full_test() { - let test_env = TestEnvironment::build_with_dummy().await; + let test_env = TestEnvironment::build(None).await; // Create a PAT for a full test let req = test::TestRequest::post() @@ -163,7 +163,7 @@ pub async fn pat_full_test() { // Test illegal PAT setting, both in POST and PATCH #[actix_rt::test] pub async fn bad_pats() { - let test_env = TestEnvironment::build_with_dummy().await; + let test_env = TestEnvironment::build(None).await; // Creating a PAT with no name should fail let req = test::TestRequest::post() diff --git a/tests/project.rs b/tests/project.rs index 29f43138..2a34dae1 100644 --- a/tests/project.rs +++ b/tests/project.rs @@ -1,9 +1,14 @@ use actix_http::StatusCode; -use actix_web::dev::ServiceResponse; use actix_web::test; +use bytes::Bytes; +use chrono::{Duration, Utc}; +use common::actix::MultipartSegment; use common::environment::with_test_environment; +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 serde_json::json; use crate::common::database::*; @@ -17,11 +22,11 @@ mod common; #[actix_rt::test] async fn test_get_project() { // Test setup and dummy data - let test_env = TestEnvironment::build_with_dummy().await; - let alpha_project_id = &test_env.dummy.as_ref().unwrap().alpha_project_id; - let beta_project_id = &test_env.dummy.as_ref().unwrap().beta_project_id; - let alpha_project_slug = &test_env.dummy.as_ref().unwrap().alpha_project_slug; - let alpha_version_id = &test_env.dummy.as_ref().unwrap().alpha_version_id; + 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() @@ -36,7 +41,6 @@ async fn test_get_project() { assert_eq!(body["id"], json!(alpha_project_id)); assert_eq!(body["slug"], json!(alpha_project_slug)); let versions = body["versions"].as_array().unwrap(); - assert!(!versions.is_empty()); assert_eq!(versions[0], json!(alpha_version_id)); // Confirm that the request was cached @@ -98,7 +102,8 @@ async fn test_get_project() { #[actix_rt::test] async fn test_add_remove_project() { // Test setup and dummy data - let test_env = TestEnvironment::build_with_dummy().await; + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; // Generate test project data. let mut json_data = json!( @@ -189,34 +194,18 @@ async fn test_add_remove_project() { assert_eq!(status, 200); // Get the project we just made, and confirm that it's correct - let req = test::TestRequest::get() - .uri("/v2/project/demo") - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - - let body: serde_json::Value = test::read_body_json(resp).await; - let versions = body["versions"].as_array().unwrap(); - assert!(versions.len() == 1); - let uploaded_version_id = &versions[0]; + 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 req = test::TestRequest::get() - .uri(&format!("/v2/version_file/{hash}?algorithm=sha1")) - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - - let body: serde_json::Value = test::read_body_json(resp).await; - let file_version_id = &body["id"]; - assert_eq!(&file_version_id, &uploaded_version_id); + 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 @@ -259,14 +248,8 @@ async fn test_add_remove_project() { assert_eq!(resp.status(), 200); // Get - let req = test::TestRequest::get() - .uri("/v2/project/demo") - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); - let body: serde_json::Value = test::read_body_json(resp).await; - let id = body["id"].to_string(); + let project = api.get_project_deserialized("demo", USER_USER_PAT).await; + let id = project.id.to_string(); // Remove the project let resp = test_env.v2.remove_project("demo", USER_USER_PAT).await; @@ -293,11 +276,7 @@ async fn test_add_remove_project() { ); // Old slug no longer works - let req = test::TestRequest::get() - .uri("/v2/project/demo") - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; + let resp = api.get_project("demo", USER_USER_PAT).await; assert_eq!(resp.status(), 404); // Cleanup test db @@ -306,219 +285,736 @@ async fn test_add_remove_project() { #[actix_rt::test] pub async fn test_patch_project() { - let test_env = TestEnvironment::build_with_dummy().await; - let alpha_project_slug = &test_env.dummy.as_ref().unwrap().alpha_project_slug; - let beta_project_slug = &test_env.dummy.as_ref().unwrap().beta_project_slug; + let test_env = TestEnvironment::build(None).await; + let api = &test_env.v2; + + 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 req = test::TestRequest::patch() - .uri(&format!("/v2/project/{alpha_project_slug}")) - .append_header(("Authorization", ENEMY_USER_PAT)) - .set_json(json!({ - "title": "Test_Add_Project project - test 1", - })) - .to_request(); - let resp = test_env.call(req).await; + 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 req = test::TestRequest::patch() - .uri(&format!("/v2/project/{alpha_project_slug}")) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - url_type: "w.fake.url", - })) - .to_request(); - let resp = test_env.call(req).await; + 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 req = test::TestRequest::patch() - .uri(&format!("/v2/project/{alpha_project_slug}")) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "requested_status": req, - })) - .to_request(); - let resp = test_env.call(req).await; + 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 req = test::TestRequest::patch() - .uri(&format!("/v2/project/{alpha_project_slug}")) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - key: "test", - })) - .to_request(); - let resp = test_env.call(req).await; + 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 req = test::TestRequest::patch() - .uri(&format!("/v2/project/{alpha_project_slug}")) - .append_header(("Authorization", MOD_USER_PAT)) - .set_json(json!({ - key: "test", - })) - .to_request(); - let resp = test_env.call(req).await; + let resp = api + .edit_project( + alpha_project_slug, + json!({ + key: "test", + }), + MOD_USER_PAT, + ) + .await; assert_eq!(resp.status(), 204); } - // Failure because the slug is already taken. - let req = test::TestRequest::patch() - .uri(&format!("/v2/project/{alpha_project_slug}")) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "slug": beta_project_slug, // the other dummy project has this slug - })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 400); + // 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 req = test::TestRequest::patch() - .uri(&format!("/v2/project/{beta_project_slug}")) - .append_header(("Authorization", USER_USER_PAT)) - .set_json(json!({ - "status": "private" - })) - .to_request(); - let resp = test_env.call(req).await; + 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", + "client_side": "optional", + "server_side": "required", + "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, Some("newslug".to_string())); + 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.client_side.to_string(), "optional"); + assert_eq!(project.server_side.to_string(), "required"); + assert_eq!(project.donation_urls.unwrap()[0].url, "https://patreon.com"); + + // 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.v2; + 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"])), + ("client_side", json!("unsupported")), + ("server_side", json!("unsupported")), + ("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!("/v2/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 + .unwrap(); + } + }) + .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!("/v2/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!( + "/v2/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!("/v2/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!("/v2/project/{alpha_project_slug}")) - .append_header(("Authorization", USER_USER_PAT)) + .uri(&format!("/v2/version/{beta_version_id}")) + .append_header(("Authorization", MOD_USER_PAT)) .set_json(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", - "client_side": "optional", - "server_side": "required", - "donation_urls": [{ - "id": "patreon", - "platform": "Patreon", - "url": "https://patreon.com" - }] + "status": "unlisted" })) .to_request(); let resp = test_env.call(req).await; assert_eq!(resp.status(), 204); - // Old slug no longer works - let req = test::TestRequest::get() - .uri(&format!("/v2/project/{alpha_project_slug}")) - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), 404); + // Schedule version + let req_gen = |_: &PermissionsTestContext| { + test::TestRequest::post() + .uri(&format!("/v2/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(); - // Old slug no longer works + // Icon edit + // Uses alpha project to delete this icon + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() + .uri(&format!( + "/v2/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!( + "/v2/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!( + "/v2/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("/v2/project/newslug") + .uri(&format!("/v2/project/{alpha_project_id}")) .append_header(("Authorization", USER_USER_PAT)) .to_request(); let resp = test_env.call(req).await; - assert_eq!(resp.status(), 200); + 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!( + "/v2/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(); - let body: serde_json::Value = test::read_body_json(resp).await; - assert_eq!(body["slug"], json!("newslug")); - assert_eq!(body["title"], json!("New successful title")); - assert_eq!(body["description"], json!("New successful description")); - assert_eq!(body["body"], json!("New successful body")); - assert_eq!(body["categories"], json!([DUMMY_CATEGORIES[0]])); - assert_eq!(body["license"]["id"], json!("MIT")); - assert_eq!(body["issues_url"], json!("https://github.com")); - assert_eq!(body["discord_url"], json!("https://discord.gg")); - assert_eq!(body["wiki_url"], json!("https://wiki.com")); - assert_eq!(body["client_side"], json!("optional")); - assert_eq!(body["server_side"], json!("required")); - assert_eq!( - body["donation_urls"][0]["url"], - json!("https://patreon.com") - ); + // Remove gallery item + // Uses alpha project to remove gallery item + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( + "/v2/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("/v2/version").set_multipart([ + MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: common::actix::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", + "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: common::actix::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!("/v2/version/{}/file", alpha_version_id)) + .set_multipart([ + MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: common::actix::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: common::actix::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!("/v2/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!("/v2/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!("/v2/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(); - // 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 alpha_project_id = &test_env.dummy.as_ref().unwrap().alpha_project_id; - let beta_project_id = &test_env.dummy.as_ref().unwrap().beta_project_id; +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!("/v2/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(); - let req = test::TestRequest::patch() + // Edit member + let edit_member = ProjectPermissions::EDIT_MEMBER; + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::patch() .uri(&format!( - "/v2/projects?ids={}", - urlencoding::encode(&format!("[\"{alpha_project_id}\",\"{beta_project_id}\"]")) + "/v2/team/{}/members/{MOD_USER_ID}", + ctx.team_id.unwrap() )) - .append_header(("Authorization", ADMIN_USER_PAT)) .set_json(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]], + "permissions": 0, })) - .to_request(); - let resp = test_env.call(req).await; - assert_eq!(resp.status(), StatusCode::NO_CONTENT); + }; + 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(); - let alpha_body = get_project_body(&test_env, &alpha_project_id, ADMIN_USER_PAT).await; - assert_eq!(alpha_body["categories"], json!(DUMMY_CATEGORIES[0..=2])); - assert_eq!( - alpha_body["additional_categories"], - json!(DUMMY_CATEGORIES[4..=5]) - ); + // remove member + // requires manage_invites if they have not yet accepted the invite + let req_gen = |ctx: &PermissionsTestContext| { + test::TestRequest::delete().uri(&format!( + "/v2/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(); - let beta_body = get_project_body(&test_env, &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; -} + // re-add member for testing + let req = test::TestRequest::post() + .uri(&format!("/v2/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); -async fn get_project( - test_env: &TestEnvironment, - project_slug: &str, - user_pat: &str, -) -> ServiceResponse { - let req = test::TestRequest::get() - .uri(&format!("/v2/project/{project_slug}")) - .append_header(("Authorization", user_pat)) + // Accept invite + let req = test::TestRequest::post() + .uri(&format!("/v2/team/{}/join", alpha_team_id)) + .append_header(("Authorization", MOD_USER_PAT)) .to_request(); - test_env.call(req).await + 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!( + "/v2/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; } -async fn get_project_body( - test_env: &TestEnvironment, - project_slug: &str, - user_pat: &str, -) -> serde_json::Value { - let resp = get_project(test_env, project_slug, user_pat).await; - assert_eq!(resp.status(), StatusCode::OK); - test::read_body_json(resp).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!("/v2/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(8)).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!("/v2/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!("/v2/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. diff --git a/tests/scopes.rs b/tests/scopes.rs index 6f86aeb7..7c11aa79 100644 --- a/tests/scopes.rs +++ b/tests/scopes.rs @@ -20,7 +20,7 @@ mod common; #[actix_rt::test] async fn user_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build_with_dummy().await; + let test_env = TestEnvironment::build(None).await; // User reading let read_user = Scopes::USER_READ; @@ -87,14 +87,20 @@ async fn user_scopes() { // Notifications #[actix_rt::test] pub async fn notifications_scopes() { - let test_env = TestEnvironment::build_with_dummy().await; - let alpha_team_id = &test_env.dummy.as_ref().unwrap().alpha_team_id.clone(); + let test_env = TestEnvironment::build(None).await; + let alpha_team_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .team_id + .clone(); // We will invite user 'friend' to project team, and use that as a notification // Get notifications let resp = test_env .v2 - .add_user_to_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) + .add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) .await; assert_eq!(resp.status(), 204); @@ -107,7 +113,7 @@ pub async fn notifications_scopes() { .test(req_gen, read_notifications) .await .unwrap(); - let notification_id = success.as_array().unwrap()[0]["id"].as_str().unwrap(); + let notification_id = success[0]["id"].as_str().unwrap(); let req_gen = || { test::TestRequest::get().uri(&format!( @@ -162,7 +168,7 @@ pub async fn notifications_scopes() { // We invite mod, get the notification ID, and do mass delete using that let resp = test_env .v2 - .add_user_to_team(alpha_team_id, MOD_USER_ID, USER_USER_PAT) + .add_user_to_team(alpha_team_id, MOD_USER_ID, None, None, USER_USER_PAT) .await; assert_eq!(resp.status(), 204); let read_notifications = Scopes::NOTIFICATION_READ; @@ -172,7 +178,7 @@ pub async fn notifications_scopes() { .test(req_gen, read_notifications) .await .unwrap(); - let notification_id = success.as_array().unwrap()[0]["id"].as_str().unwrap(); + let notification_id = success[0]["id"].as_str().unwrap(); let req_gen = || { test::TestRequest::delete().uri(&format!( @@ -193,7 +199,7 @@ pub async fn notifications_scopes() { // Project version creation scopes #[actix_rt::test] pub async fn project_version_create_scopes() { - let test_env = TestEnvironment::build_with_dummy().await; + let test_env = TestEnvironment::build(None).await; // Create project let create_project = Scopes::PROJECT_CREATE; @@ -292,11 +298,35 @@ pub async fn project_version_create_scopes() { // Project management scopes #[actix_rt::test] pub async fn project_version_reads_scopes() { - let test_env = TestEnvironment::build_with_dummy().await; - let beta_project_id = &test_env.dummy.as_ref().unwrap().beta_project_id.clone(); - let beta_version_id = &test_env.dummy.as_ref().unwrap().beta_version_id.clone(); - let alpha_team_id = &test_env.dummy.as_ref().unwrap().alpha_team_id.clone(); - let beta_file_hash = &test_env.dummy.as_ref().unwrap().beta_file_hash.clone(); + let test_env = TestEnvironment::build(None).await; + let beta_project_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .project_id + .clone(); + let beta_version_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .version_id + .clone(); + let alpha_team_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .team_id + .clone(); + let beta_file_hash = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .file_hash + .clone(); // Project reading // Uses 404 as the expected failure code (or 200 and an empty list for mass reads) @@ -348,8 +378,8 @@ pub async fn project_version_reads_scopes() { .test(req_gen, read_project) .await .unwrap(); - assert!(!failure.as_array().unwrap()[0].as_object().unwrap()["permissions"].is_number()); - assert!(success.as_array().unwrap()[0].as_object().unwrap()["permissions"].is_number()); + assert!(!failure[0]["permissions"].is_number()); + assert!(success[0]["permissions"].is_number()); let req_gen = || { test::TestRequest::get().uri(&format!( @@ -362,14 +392,8 @@ pub async fn project_version_reads_scopes() { .test(req_gen, read_project) .await .unwrap(); - assert!(!failure.as_array().unwrap()[0].as_array().unwrap()[0] - .as_object() - .unwrap()["permissions"] - .is_number()); - assert!(success.as_array().unwrap()[0].as_array().unwrap()[0] - .as_object() - .unwrap()["permissions"] - .is_number()); + assert!(!failure[0][0]["permissions"].is_number()); + assert!(success[0][0]["permissions"].is_number()); // User project reading // Test user has two projects, one public and one private @@ -510,9 +534,21 @@ pub async fn project_version_reads_scopes() { #[actix_rt::test] pub async fn project_write_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build_with_dummy().await; - let beta_project_id = &test_env.dummy.as_ref().unwrap().beta_project_id.clone(); - let alpha_team_id = &test_env.dummy.as_ref().unwrap().alpha_team_id.clone(); + let test_env = TestEnvironment::build(None).await; + let beta_project_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .project_id + .clone(); + let alpha_team_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .team_id + .clone(); // Projects writing let write_project = Scopes::PROJECT_WRITE; @@ -714,10 +750,28 @@ pub async fn project_write_scopes() { #[actix_rt::test] pub async fn version_write_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build_with_dummy().await; - let alpha_version_id = &test_env.dummy.as_ref().unwrap().beta_version_id.clone(); - let beta_version_id = &test_env.dummy.as_ref().unwrap().beta_version_id.clone(); - let alpha_file_hash = &test_env.dummy.as_ref().unwrap().beta_file_hash.clone(); + let test_env = TestEnvironment::build(None).await; + let alpha_version_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .version_id + .clone(); + let beta_version_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .version_id + .clone(); + let alpha_file_hash = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .file_hash + .clone(); let write_version = Scopes::VERSION_WRITE; @@ -829,8 +883,14 @@ pub async fn version_write_scopes() { #[actix_rt::test] pub async fn report_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build_with_dummy().await; - let beta_project_id = &test_env.dummy.as_ref().unwrap().beta_project_id.clone(); + let test_env = TestEnvironment::build(None).await; + let beta_project_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .project_id + .clone(); // Create report let report_create = Scopes::REPORT_CREATE; @@ -854,7 +914,7 @@ pub async fn report_scopes() { .test(req_gen, report_read) .await .unwrap(); - let report_id = success.as_array().unwrap()[0]["id"].as_str().unwrap(); + let report_id = success[0]["id"].as_str().unwrap(); let req_gen = || test::TestRequest::get().uri(&format!("/v2/report/{}", report_id)); ScopeTest::new(&test_env) @@ -905,9 +965,21 @@ pub async fn report_scopes() { #[actix_rt::test] pub async fn thread_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build_with_dummy().await; - let alpha_thread_id = &test_env.dummy.as_ref().unwrap().alpha_thread_id.clone(); - let beta_thread_id = &test_env.dummy.as_ref().unwrap().beta_thread_id.clone(); + let test_env = TestEnvironment::build(None).await; + let alpha_thread_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .thread_id + .clone(); + let beta_thread_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .thread_id + .clone(); // Thread read let thread_read = Scopes::THREAD_READ; @@ -954,8 +1026,7 @@ pub async fn thread_scopes() { .test(req_gen, thread_read) .await .unwrap(); - let thread = success.as_array().unwrap()[0].as_object().unwrap(); - let thread_id = thread["id"].as_str().unwrap(); + let thread_id = success[0]["id"].as_str().unwrap(); // Moderator 'read' thread // Uses moderator PAT, as only moderators can see the moderation inbox @@ -974,10 +1045,8 @@ pub async fn thread_scopes() { .to_request(); let resp = test_env.call(req_gen).await; let success: serde_json::Value = test::read_body_json(resp).await; - let thread_messages = success.as_object().unwrap()["messages"].as_array().unwrap(); - let thread_message_id = thread_messages[0].as_object().unwrap()["id"] - .as_str() - .unwrap(); + let thread_message_id = success["messages"][0]["id"].as_str().unwrap(); + let req_gen = || test::TestRequest::delete().uri(&format!("/v2/message/{thread_message_id}")); ScopeTest::new(&test_env) .with_user_id(MOD_USER_ID_PARSED) @@ -992,7 +1061,7 @@ pub async fn thread_scopes() { // Pat scopes #[actix_rt::test] pub async fn pat_scopes() { - let test_env = TestEnvironment::build_with_dummy().await; + let test_env = TestEnvironment::build(None).await; // Pat create let pat_create = Scopes::PAT_CREATE; @@ -1045,8 +1114,14 @@ pub async fn pat_scopes() { #[actix_rt::test] pub async fn collections_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build_with_dummy().await; - let alpha_project_id = &test_env.dummy.as_ref().unwrap().alpha_project_id.clone(); + let test_env = TestEnvironment::build(None).await; + let alpha_project_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_alpha + .project_id + .clone(); // Create collection let collection_create = Scopes::COLLECTION_CREATE; @@ -1140,8 +1215,14 @@ pub async fn collections_scopes() { #[actix_rt::test] pub async fn organization_scopes() { // Test setup and dummy data - let test_env = TestEnvironment::build_with_dummy().await; - let beta_project_id = &test_env.dummy.as_ref().unwrap().beta_project_id.clone(); + let test_env = TestEnvironment::build(None).await; + let beta_project_id = &test_env + .dummy + .as_ref() + .unwrap() + .project_beta + .project_id + .clone(); // Create organization let organization_create = Scopes::ORGANIZATION_CREATE; @@ -1215,18 +1296,8 @@ pub async fn organization_scopes() { .test(req_gen, organization_read) .await .unwrap(); - assert!( - failure.as_object().unwrap()["members"].as_array().unwrap()[0] - .as_object() - .unwrap()["permissions"] - .is_null() - ); - assert!( - !success.as_object().unwrap()["members"].as_array().unwrap()[0] - .as_object() - .unwrap()["permissions"] - .is_null() - ); + assert!(failure["members"][0]["permissions"].is_null()); + assert!(!success["members"][0]["permissions"].is_null()); let req_gen = || { test::TestRequest::get().uri(&format!( @@ -1240,22 +1311,8 @@ pub async fn organization_scopes() { .test(req_gen, organization_read) .await .unwrap(); - assert!( - failure.as_array().unwrap()[0].as_object().unwrap()["members"] - .as_array() - .unwrap()[0] - .as_object() - .unwrap()["permissions"] - .is_null() - ); - assert!( - !success.as_array().unwrap()[0].as_object().unwrap()["members"] - .as_array() - .unwrap()[0] - .as_object() - .unwrap()["permissions"] - .is_null() - ); + assert!(failure[0]["members"][0]["permissions"].is_null()); + assert!(!success[0]["members"][0]["permissions"].is_null()); let organization_project_read = Scopes::PROJECT_READ | Scopes::ORGANIZATION_READ; let req_gen = @@ -1300,6 +1357,4 @@ pub async fn organization_scopes() { // TODO: Some hash/version files functions -// TODO: Meta pat stuff - // TODO: Image scopes diff --git a/tests/teams.rs b/tests/teams.rs new file mode 100644 index 00000000..9b00fbdc --- /dev/null +++ b/tests/teams.rs @@ -0,0 +1,661 @@ +use actix_web::test; +use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; +use serde_json::json; + +use crate::common::database::*; + +use crate::common::environment::TestEnvironment; + +// importing common module. +mod common; + +#[actix_rt::test] +async fn test_get_team() { + // 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 alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + // Perform tests for an organization team and a project team + for (team_association_id, team_association, team_id) in [ + (alpha_project_id, "project", alpha_team_id), + (zeta_organization_id, "organization", zeta_team_id), + ] { + // A non-member of the team should get basic info but not be able to see private data + for uri in [ + format!("/v2/team/{team_id}/members"), + format!("/v2/{team_association}/{team_association_id}/members"), + ] { + let req = test::TestRequest::get() + .uri(&uri) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + assert_eq!(value[0]["user"]["id"], USER_USER_ID); + assert!(value[0]["permissions"].is_null()); + } + + // A non-accepted member of the team should: + // - not be able to see private data about the team, but see all members including themselves + // - should not appear in the team members list to enemy users + let req = test::TestRequest::post() + .uri(&format!("/v2/team/{team_id}/members")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(&json!({ + "user_id": FRIEND_USER_ID, + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + for uri in [ + format!("/v2/team/{team_id}/members"), + format!("/v2/{team_association}/{team_association_id}/members"), + ] { + let req = test::TestRequest::get() + .uri(&uri) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let members = value.as_array().unwrap(); + assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team + let user_user = members + .iter() + .find(|x| x["user"]["id"] == USER_USER_ID) + .unwrap(); + let friend_user = members + .iter() + .find(|x| x["user"]["id"] == FRIEND_USER_ID) + .unwrap(); + assert_eq!(user_user["user"]["id"], USER_USER_ID); + assert!(user_user["permissions"].is_null()); // Should not see private data of the team + assert_eq!(friend_user["user"]["id"], FRIEND_USER_ID); + assert!(friend_user["permissions"].is_null()); + + let req = test::TestRequest::get() + .uri(&uri) + .append_header(("Authorization", ENEMY_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let members = value.as_array().unwrap(); + assert_eq!(members.len(), 1); // Only USER_USER_ID should be in the team + assert_eq!(members[0]["user"]["id"], USER_USER_ID); + assert!(members[0]["permissions"].is_null()); + } + // An accepted member of the team should appear in the team members list + // and should be able to see private data about the team + let req = test::TestRequest::post() + .uri(&format!("/v2/team/{team_id}/join")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + for uri in [ + format!("/v2/team/{team_id}/members"), + format!("/v2/{team_association}/{team_association_id}/members"), + ] { + let req = test::TestRequest::get() + .uri(&uri) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let members = value.as_array().unwrap(); + assert!(members.len() == 2); // USER_USER_ID and FRIEND_USER_ID should be in the team + let user_user = members + .iter() + .find(|x| x["user"]["id"] == USER_USER_ID) + .unwrap(); + let friend_user = members + .iter() + .find(|x| x["user"]["id"] == FRIEND_USER_ID) + .unwrap(); + assert_eq!(user_user["user"]["id"], USER_USER_ID); + assert!(!user_user["permissions"].is_null()); // SHOULD see private data of the team + assert_eq!(friend_user["user"]["id"], FRIEND_USER_ID); + assert!(!friend_user["permissions"].is_null()); + } + } + + // Cleanup test db + test_env.cleanup().await; +} + +#[actix_rt::test] +async fn test_get_team_project_orgs() { + // 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 alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + let zeta_organization_id = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + // Attach alpha to zeta + let req = test::TestRequest::post() + .uri(&format!("/v2/organization/{zeta_organization_id}/projects")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "project_id": alpha_project_id, + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + + // Invite and add friend to zeta + let req = test::TestRequest::post() + .uri(&format!("/v2/team/{zeta_team_id}/members")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "user_id": FRIEND_USER_ID, + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + let req = test::TestRequest::post() + .uri(&format!("/v2/team/{zeta_team_id}/join")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // The team members route from teams (on a project's team): + // - the members of the project team specifically + // - not the ones from the organization + let req = test::TestRequest::get() + .uri(&format!("/v2/team/{alpha_team_id}/members")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let members = value.as_array().unwrap(); + assert_eq!(members.len(), 1); + + // The team members route from project should show: + // - the members of the project team including the ones from the organization + let req = test::TestRequest::get() + .uri(&format!("/v2/project/{alpha_project_id}/members")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let members = value.as_array().unwrap(); + assert_eq!(members.len(), 2); + + // Cleanup test db + test_env.cleanup().await; +} + +// edit team member (Varying permissions, varying roles) +#[actix_rt::test] +async fn test_patch_project_team_member() { + // Test setup and dummy data + let test_env = TestEnvironment::build(None).await; + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + + // Edit team as admin/mod but not a part of the team should be OK + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{alpha_team_id}/members/{USER_USER_ID}")) + .set_json(json!({})) + .append_header(("Authorization", ADMIN_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // As a non-owner with full permissions, attempt to edit the owner's permissions/roles + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{alpha_team_id}/members/{USER_USER_ID}")) + .append_header(("Authorization", ADMIN_USER_PAT)) + .set_json(json!({ + "role": "member" + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{alpha_team_id}/members/{USER_USER_ID}")) + .append_header(("Authorization", ADMIN_USER_PAT)) + .set_json(json!({ + "permissions": 0 + })) + .to_request(); + let resp = test_env.call(req).await; + + assert_eq!(resp.status(), 400); + + // Should not be able to edit organization permissions of a project team + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{alpha_team_id}/members/{USER_USER_ID}")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "organization_permissions": 0 + })) + .to_request(); + let resp = test_env.call(req).await; + + assert_eq!(resp.status(), 400); + + // Should not be able to add permissions to a user that the adding-user does not have + // (true for both project and org) + + // first, invite friend + let req = test::TestRequest::post() + .uri(&format!("/v2/team/{alpha_team_id}/members")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(&json!({ + "user_id": FRIEND_USER_ID, + "permissions": (ProjectPermissions::EDIT_MEMBER | ProjectPermissions::EDIT_BODY).bits(), + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // accept + let req = test::TestRequest::post() + .uri(&format!("/v2/team/{alpha_team_id}/join")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // try to add permissions + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{alpha_team_id}/members/{FRIEND_USER_ID}")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .set_json(json!({ + "permissions": (ProjectPermissions::EDIT_MEMBER | ProjectPermissions::EDIT_DETAILS).bits() + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + + // Cannot set a user to Owner + let req = test::TestRequest::patch() + .uri(&format!( + "/v2/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + )) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "role": "Owner" + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + + // Cannot set payouts outside of 0 and 5000 + for payout in [-1, 5001] { + let req = test::TestRequest::patch() + .uri(&format!( + "/v2/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + )) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "payouts_split": payout + })) + .to_request(); + let resp = test_env.call(req).await; + + assert_eq!(resp.status(), 400); + } + + // Successful patch + let req = test::TestRequest::patch() + .uri(&format!( + "/v2/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + )) + .append_header(("Authorization", FRIEND_USER_PAT)) + .set_json(json!({ + "payouts_split": 51, + "permissions": ProjectPermissions::EDIT_MEMBER.bits(), // reduces permissions + "role": "member", + "ordering": 5 + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // Check results + let req = test::TestRequest::get() + .uri(&format!("/v2/team/{alpha_team_id}/members")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let member = value + .as_array() + .unwrap() + .iter() + .find(|x| x["user"]["id"] == FRIEND_USER_ID) + .unwrap(); + assert_eq!(member["payouts_split"], 51.0); + assert_eq!( + member["permissions"], + ProjectPermissions::EDIT_MEMBER.bits() + ); + assert_eq!(member["role"], "member"); + assert_eq!(member["ordering"], 5); + + // Cleanup test db + test_env.cleanup().await; +} + +// edit team member (Varying permissions, varying roles) +#[actix_rt::test] +async fn test_patch_organization_team_member() { + // Test setup and dummy data + let test_env = TestEnvironment::build(None).await; + let zeta_team_id = &test_env.dummy.as_ref().unwrap().organization_zeta.team_id; + + // Edit team as admin/mod but not a part of the team should be OK + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{zeta_team_id}/members/{USER_USER_ID}")) + .set_json(json!({})) + .append_header(("Authorization", ADMIN_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // As a non-owner with full permissions, attempt to edit the owner's permissions/roles + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{zeta_team_id}/members/{USER_USER_ID}")) + .append_header(("Authorization", ADMIN_USER_PAT)) + .set_json(json!({ + "role": "member" + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{zeta_team_id}/members/{USER_USER_ID}")) + .append_header(("Authorization", ADMIN_USER_PAT)) + .set_json(json!({ + "permissions": 0 + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + + // Should not be able to add permissions to a user that the adding-user does not have + // (true for both project and org) + + // first, invite friend + let req = test::TestRequest::post() + .uri(&format!("/v2/team/{zeta_team_id}/members")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(&json!({ + "user_id": FRIEND_USER_ID, + "organization_permissions": (OrganizationPermissions::EDIT_MEMBER | OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS).bits(), + })).to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // accept + let req = test::TestRequest::post() + .uri(&format!("/v2/team/{zeta_team_id}/join")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // try to add permissions- fails, as we do not have EDIT_DETAILS + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .set_json(json!({ + "organization_permissions": (OrganizationPermissions::EDIT_MEMBER | OrganizationPermissions::EDIT_DETAILS).bits() + })) + .to_request(); + let resp = test_env.call(req).await; + + assert_eq!(resp.status(), 400); + + // Cannot set a user to Owner + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "role": "Owner" + })) + .to_request(); + let resp = test_env.call(req).await; + + assert_eq!(resp.status(), 400); + + // Cannot set payouts outside of 0 and 5000 + for payout in [-1, 5001] { + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "payouts_split": payout + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 400); + } + + // Successful patch + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{zeta_team_id}/members/{FRIEND_USER_ID}")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .set_json(json!({ + "payouts_split": 51, + "organization_permissions": (OrganizationPermissions::EDIT_MEMBER).bits(), // reduces permissions + "permissions": (ProjectPermissions::EDIT_MEMBER).bits(), + "role": "member", + "ordering": 5 + })) + .to_request(); + let resp = test_env.call(req).await; + + assert_eq!(resp.status(), 204); + + // Check results + let req = test::TestRequest::get() + .uri(&format!("/v2/team/{zeta_team_id}/members")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let member = value + .as_array() + .unwrap() + .iter() + .find(|x| x["user"]["id"] == FRIEND_USER_ID) + .unwrap(); + assert_eq!(member["payouts_split"], 51.0); + assert_eq!( + member["organization_permissions"], + OrganizationPermissions::EDIT_MEMBER.bits() + ); + assert_eq!( + member["permissions"], + ProjectPermissions::EDIT_MEMBER.bits() + ); + assert_eq!(member["role"], "member"); + assert_eq!(member["ordering"], 5); + + // Cleanup test db + test_env.cleanup().await; +} + +// trasnfer ownership (requires being owner, etc) +#[actix_rt::test] +async fn transfer_ownership() { + // Test setup and dummy data + let test_env = TestEnvironment::build(None).await; + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + + // Cannot set friend as owner (not a member) + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{alpha_team_id}/owner")) + .set_json(json!({ + "user_id": FRIEND_USER_ID + })) + .append_header(("Authorization", USER_USER_ID)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 401); + + // first, invite friend + let req = test::TestRequest::post() + .uri(&format!("/v2/team/{alpha_team_id}/members")) + .append_header(("Authorization", USER_USER_PAT)) + .set_json(json!({ + "user_id": FRIEND_USER_ID, + })) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // accept + let req = test::TestRequest::post() + .uri(&format!("/v2/team/{alpha_team_id}/join")) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // Cannot set ourselves as owner + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{alpha_team_id}/owner")) + .set_json(json!({ + "user_id": FRIEND_USER_ID + })) + .append_header(("Authorization", FRIEND_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 401); + + // Can set friend as owner + let req = test::TestRequest::patch() + .uri(&format!("/v2/team/{alpha_team_id}/owner")) + .set_json(json!({ + "user_id": FRIEND_USER_ID + })) + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 204); + + // Check + let req = test::TestRequest::get() + .uri(&format!("/v2/team/{alpha_team_id}/members")) + .set_json(json!({ + "user_id": FRIEND_USER_ID + })) + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 200); + let value: serde_json::Value = test::read_body_json(resp).await; + let friend_member = value + .as_array() + .unwrap() + .iter() + .find(|x| x["user"]["id"] == FRIEND_USER_ID) + .unwrap(); + assert_eq!(friend_member["role"], "Owner"); + assert_eq!( + friend_member["permissions"], + ProjectPermissions::all().bits() + ); + let user_member = value + .as_array() + .unwrap() + .iter() + .find(|x| x["user"]["id"] == USER_USER_ID) + .unwrap(); + assert_eq!(user_member["role"], "Member"); + assert_eq!(user_member["permissions"], ProjectPermissions::all().bits()); + + // Confirm that user, a user who still has full permissions, cannot then remove the owner + let req = test::TestRequest::delete() + .uri(&format!( + "/v2/team/{alpha_team_id}/members/{FRIEND_USER_ID}" + )) + .append_header(("Authorization", USER_USER_PAT)) + .to_request(); + + let resp = test_env.call(req).await; + assert_eq!(resp.status(), 401); + + // Cleanup test db + test_env.cleanup().await; +} + +// This test is currently not working. +// #[actix_rt::test] +// pub async fn no_acceptance_permissions() { +// // Adding a user to a project team in an organization, when that user is in the organization but not the team, +// // should have those permissions apply regardless of whether the user has accepted the invite or not. + +// // This is because project-team permission overrriding must be possible, and this overriding can decrease the number of permissions a user has. + +// let test_env = TestEnvironment::build(None).await; +// let api = &test_env.v2; + +// let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; +// let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; +// let zeta_organization_id = &test_env.dummy.as_ref().unwrap().zeta_organization_id; +// let zeta_team_id = &test_env.dummy.as_ref().unwrap().zeta_team_id; + +// // Link alpha team to zeta org +// let resp = api.organization_add_project(zeta_organization_id, alpha_project_id, USER_USER_PAT).await; +// assert_eq!(resp.status(), 200); + +// // Invite friend to zeta team with all project default permissions +// let resp = api.add_user_to_team(&zeta_team_id, FRIEND_USER_ID, Some(ProjectPermissions::all()), Some(OrganizationPermissions::all()), USER_USER_PAT).await; +// assert_eq!(resp.status(), 204); + +// // Accept invite to zeta team +// let resp = api.join_team(&zeta_team_id, FRIEND_USER_PAT).await; +// assert_eq!(resp.status(), 204); + +// // Attempt, as friend, to edit details of alpha project (should succeed, org invite accepted) +// let resp = api.edit_project(alpha_project_id, json!({ +// "title": "new name" +// }), FRIEND_USER_PAT).await; +// assert_eq!(resp.status(), 204); + +// // Invite friend to alpha team with *no* project permissions +// let resp = api.add_user_to_team(&alpha_team_id, FRIEND_USER_ID, Some(ProjectPermissions::empty()), None, USER_USER_PAT).await; +// assert_eq!(resp.status(), 204); + +// // Do not accept invite to alpha team + +// // Attempt, as friend, to edit details of alpha project (should fail now, even though user has not accepted invite) +// let resp = api.edit_project(alpha_project_id, json!({ +// "title": "new name" +// }), FRIEND_USER_PAT).await; +// assert_eq!(resp.status(), 401); + +// test_env.cleanup().await; +// } diff --git a/tests/user.rs b/tests/user.rs index efb1f920..664bbdc1 100644 --- a/tests/user.rs +++ b/tests/user.rs @@ -7,6 +7,15 @@ use crate::common::{dummy_data::DummyJarFile, request_data::get_public_project_c mod common; +// user GET (permissions, different users) +// users GET +// user auth +// user projects get +// user collections get +// patch user +// patch user icon +// user follows + #[actix_rt::test] pub async fn get_user_projects_after_creating_project_returns_new_project() { with_test_environment(|test_env| async move { @@ -15,10 +24,10 @@ pub async fn get_user_projects_after_creating_project_returns_new_project() { .await; let (project, _) = api - .add_public_project(get_public_project_creation_data( - "slug", - DummyJarFile::BasicMod, - )) + .add_public_project( + get_public_project_creation_data("slug", Some(DummyJarFile::BasicMod)), + USER_USER_PAT, + ) .await; let resp_projects = api @@ -34,15 +43,15 @@ pub async fn get_user_projects_after_deleting_project_shows_removal() { with_test_environment(|test_env| async move { let api = test_env.v2; let (project, _) = api - .add_public_project(get_public_project_creation_data( - "iota", - DummyJarFile::BasicMod, - )) + .add_public_project( + get_public_project_creation_data("iota", Some(DummyJarFile::BasicMod)), + USER_USER_PAT, + ) .await; api.get_user_projects_deserialized(USER_USER_ID, USER_USER_PAT) .await; - api.remove_project(&project.slug.as_ref().unwrap(), USER_USER_PAT) + api.remove_project(project.slug.as_ref().unwrap(), USER_USER_PAT) .await; let resp_projects = api @@ -56,15 +65,15 @@ pub async fn get_user_projects_after_deleting_project_shows_removal() { #[actix_rt::test] pub async fn get_user_projects_after_joining_team_shows_team_projects() { with_test_environment(|test_env| async move { - let alpha_team_id = &test_env.dummy.as_ref().unwrap().alpha_team_id; - let alpha_project_id = &test_env.dummy.as_ref().unwrap().alpha_project_id; + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let api = test_env.v2; api.get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) .await; - api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) + api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) .await; - api.join_team(&alpha_team_id, FRIEND_USER_PAT).await; + api.join_team(alpha_team_id, FRIEND_USER_PAT).await; let projects = api .get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) @@ -79,16 +88,16 @@ pub async fn get_user_projects_after_joining_team_shows_team_projects() { #[actix_rt::test] pub async fn get_user_projects_after_leaving_team_shows_no_team_projects() { with_test_environment(|test_env| async move { - let alpha_team_id = &test_env.dummy.as_ref().unwrap().alpha_team_id; - let alpha_project_id = &test_env.dummy.as_ref().unwrap().alpha_project_id; + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id; let api = test_env.v2; - api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) + api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT) .await; - api.join_team(&alpha_team_id, FRIEND_USER_PAT).await; + api.join_team(alpha_team_id, FRIEND_USER_PAT).await; api.get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) .await; - api.remove_from_team(&alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) + api.remove_from_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT) .await; let projects = api