diff --git a/migrations/20231124070100_renaming_consistency.sql b/migrations/20231124070100_renaming_consistency.sql index d1c85390..272ec809 100644 --- a/migrations/20231124070100_renaming_consistency.sql +++ b/migrations/20231124070100_renaming_consistency.sql @@ -9,4 +9,8 @@ ALTER TABLE notifications_actions RENAME COLUMN title TO name; -- rename project 'description' to 'summary' -- rename project 'body' to 'description' ALTER TABLE mods RENAME COLUMN description TO summary; -ALTER TABLE mods RENAME COLUMN body TO description; \ No newline at end of file +ALTER TABLE mods RENAME COLUMN body TO description; + +-- Adds 'is_owner' boolean to team members table- only one can be true. +ALTER TABLE team_members ADD COLUMN is_owner boolean NOT NULL DEFAULT false; +UPDATE team_members SET is_owner = true WHERE role = 'Owner'; \ No newline at end of file diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs index a0a92f70..704d229a 100644 --- a/src/database/models/team_item.rs +++ b/src/database/models/team_item.rs @@ -15,6 +15,7 @@ pub struct TeamBuilder { pub struct TeamMemberBuilder { pub user_id: UserId, pub role: String, + pub is_owner: bool, pub permissions: ProjectPermissions, pub organization_permissions: Option, pub accepted: bool, @@ -50,6 +51,7 @@ impl TeamBuilder { team_ids, user_ids, roles, + is_owners, permissions, organization_permissions, accepteds, @@ -64,6 +66,7 @@ impl TeamBuilder { Vec<_>, Vec<_>, Vec<_>, + Vec<_>, ) = members .into_iter() .map(|m| { @@ -71,6 +74,7 @@ impl TeamBuilder { team.id.0, m.user_id.0, m.role, + m.is_owner, m.permissions.bits() as i64, m.organization_permissions.map(|p| p.bits() as i64), m.accepted, @@ -81,13 +85,14 @@ impl TeamBuilder { .multiunzip(); sqlx::query!( " - INSERT INTO team_members (id, team_id, user_id, role, permissions, organization_permissions, accepted, payouts_split, ordering) - SELECT * FROM UNNEST ($1::int8[], $2::int8[], $3::int8[], $4::varchar[], $5::int8[], $6::int8[], $7::bool[], $8::numeric[], $9::int8[]) + INSERT INTO team_members (id, team_id, user_id, role, is_owner, permissions, organization_permissions, accepted, payouts_split, ordering) + SELECT * FROM UNNEST ($1::int8[], $2::int8[], $3::int8[], $4::varchar[], $5::bool[], $6::int8[], $7::int8[], $8::bool[], $9::numeric[], $10::int8[]) ", &team_member_ids[..], &team_ids[..], &user_ids[..], &roles[..], + &is_owners[..], &permissions[..], &organization_permissions[..] as &[Option], &accepteds[..], @@ -162,6 +167,7 @@ pub struct TeamMember { /// The ID of the user associated with the member pub user_id: UserId, pub role: String, + pub is_owner: bool, // The permissions of the user in this project team // For an organization team, these are the fallback permissions for any project in the organization @@ -233,7 +239,7 @@ impl TeamMember { if !team_ids_parsed.is_empty() { let teams: Vec = sqlx::query!( " - SELECT id, team_id, role AS member_role, permissions, organization_permissions, + SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions, accepted, payouts_split, ordering, user_id FROM team_members @@ -248,6 +254,7 @@ impl TeamMember { id: TeamMemberId(m.id), team_id: TeamId(m.team_id), role: m.member_role, + is_owner: m.is_owner, permissions: ProjectPermissions::from_bits(m.permissions as u64) .unwrap_or_default(), organization_permissions: m @@ -310,7 +317,7 @@ impl TeamMember { let team_members = sqlx::query!( " - SELECT id, team_id, role AS member_role, permissions, organization_permissions, + SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions, accepted, payouts_split, role, ordering, user_id FROM team_members @@ -328,6 +335,7 @@ impl TeamMember { team_id: TeamId(m.team_id), user_id, role: m.role, + is_owner: m.is_owner, permissions: ProjectPermissions::from_bits(m.permissions as u64) .unwrap_or_default(), organization_permissions: m @@ -362,7 +370,7 @@ impl TeamMember { { let result = sqlx::query!( " - SELECT id, team_id, role AS member_role, permissions, organization_permissions, + SELECT id, team_id, role AS member_role, is_owner, permissions, organization_permissions, accepted, payouts_split, role, ordering, user_id @@ -382,6 +390,7 @@ impl TeamMember { team_id: id, user_id, role: m.role, + is_owner: m.is_owner, permissions: ProjectPermissions::from_bits(m.permissions as u64) .unwrap_or_default(), organization_permissions: m @@ -431,11 +440,10 @@ impl TeamMember { sqlx::query!( " DELETE FROM team_members - WHERE (team_id = $1 AND user_id = $2 AND NOT role = $3) + WHERE (team_id = $1 AND user_id = $2 AND NOT is_owner = TRUE) ", id as TeamId, user_id as UserId, - crate::models::teams::OWNER_ROLE, ) .execute(&mut **transaction) .await?; @@ -453,6 +461,7 @@ impl TeamMember { new_accepted: Option, new_payouts_split: Option, new_ordering: Option, + new_is_owner: Option, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result<(), super::DatabaseError> { if let Some(permissions) = new_permissions { @@ -546,6 +555,21 @@ impl TeamMember { .await?; } + if let Some(is_owner) = new_is_owner { + sqlx::query!( + " + UPDATE team_members + SET is_owner = $1 + WHERE (team_id = $2 AND user_id = $3) + ", + is_owner, + id as TeamId, + user_id as UserId, + ) + .execute(&mut **transaction) + .await?; + } + Ok(()) } @@ -559,7 +583,7 @@ impl TeamMember { { let result = sqlx::query!( " - SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering + SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id AND user_id = $2 AND accepted = TRUE WHERE m.id = $1 @@ -576,6 +600,7 @@ impl TeamMember { team_id: TeamId(m.team_id), user_id, role: m.role, + is_owner: m.is_owner, permissions: ProjectPermissions::from_bits(m.permissions as u64) .unwrap_or_default(), organization_permissions: m @@ -600,7 +625,7 @@ impl TeamMember { { let result = sqlx::query!( " - SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering + SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering FROM organizations o INNER JOIN team_members tm ON tm.team_id = o.team_id AND user_id = $2 AND accepted = TRUE WHERE o.id = $1 @@ -617,6 +642,7 @@ impl TeamMember { team_id: TeamId(m.team_id), user_id, role: m.role, + is_owner: m.is_owner, permissions: ProjectPermissions::from_bits(m.permissions as u64) .unwrap_or_default(), organization_permissions: m @@ -641,7 +667,7 @@ impl TeamMember { { let result = sqlx::query!( " - SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering, v.mod_id + SELECT tm.id, tm.team_id, tm.user_id, tm.role, tm.is_owner, tm.permissions, tm.organization_permissions, tm.accepted, tm.payouts_split, tm.ordering, v.mod_id FROM versions v INNER JOIN mods m ON m.id = v.mod_id INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.user_id = $2 AND tm.accepted = TRUE @@ -659,6 +685,7 @@ impl TeamMember { team_id: TeamId(m.team_id), user_id, role: m.role, + is_owner: m.is_owner, permissions: ProjectPermissions::from_bits(m.permissions as u64) .unwrap_or_default(), organization_permissions: m diff --git a/src/database/models/user_item.rs b/src/database/models/user_item.rs index 8230ff58..571479d2 100644 --- a/src/database/models/user_item.rs +++ b/src/database/models/user_item.rs @@ -442,10 +442,9 @@ impl User { " SELECT m.id FROM mods m INNER JOIN team_members tm ON tm.team_id = m.team_id - WHERE tm.user_id = $1 AND tm.role = $2 + WHERE tm.user_id = $1 AND tm.is_owner = TRUE ", id as UserId, - crate::models::teams::OWNER_ROLE ) .fetch_many(&mut **transaction) .try_filter_map(|e| async { Ok(e.right().map(|m| ProjectId(m.id))) }) @@ -462,11 +461,10 @@ impl User { " UPDATE team_members SET user_id = $1 - WHERE (user_id = $2 AND role = $3) + WHERE (user_id = $2 AND is_owner = TRUE) ", deleted_user as UserId, id as UserId, - crate::models::teams::OWNER_ROLE ) .execute(&mut **transaction) .await?; diff --git a/src/models/v2/mod.rs b/src/models/v2/mod.rs index d261d2b7..ae3a0b08 100644 --- a/src/models/v2/mod.rs +++ b/src/models/v2/mod.rs @@ -1,3 +1,4 @@ // Legacy models from V2, where its useful to keep the struct for rerouting/conversion pub mod projects; pub mod notifications; +pub mod teams; diff --git a/src/models/v2/teams.rs b/src/models/v2/teams.rs new file mode 100644 index 00000000..d05ee74a --- /dev/null +++ b/src/models/v2/teams.rs @@ -0,0 +1,40 @@ +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; + +use crate::models::{ids::TeamId, users::User, teams::{ProjectPermissions, OrganizationPermissions, TeamMember}}; + +/// A member of a team +#[derive(Serialize, Deserialize, Clone)] +pub struct LegacyTeamMember { + pub role: String, + // is_owner removed, and role hardcoded to Owner if true, + + pub team_id: TeamId, + pub user: User, + pub permissions: Option, + pub organization_permissions: Option, // TODO: technically not a v2 field, should it be kept? + pub accepted: bool, + + #[serde(with = "rust_decimal::serde::float_option")] + pub payouts_split: Option, + pub ordering: i64, +} + +impl LegacyTeamMember { + pub fn from(team_member : TeamMember) -> Self { + LegacyTeamMember { + role: match (team_member.is_owner, team_member.role.as_str()) { + (true, _) => "Owner".to_string(), + (false, "Owner") => "Member".to_string(), // The odd case of a non-owner with the owner role should show as 'Member' + (false, role) => role.to_string(), + }, + team_id: team_member.team_id, + user: team_member.user, + permissions: team_member.permissions, + organization_permissions: team_member.organization_permissions, + accepted: team_member.accepted, + payouts_split: team_member.payouts_split, + ordering: team_member.ordering, + } + } +} \ No newline at end of file diff --git a/src/models/v3/teams.rs b/src/models/v3/teams.rs index ad3c6e70..814705e1 100644 --- a/src/models/v3/teams.rs +++ b/src/models/v3/teams.rs @@ -135,6 +135,8 @@ pub struct TeamMember { pub user: User, /// The role of the user in the team pub role: String, + /// Is the user the owner of the team? + pub is_owner: bool, /// A bitset containing the user's permissions in this team. /// In an organization-controlled project, these are the unique overriding permissions for the user's role for any project in the organization, if they exist. /// In an organization, these are the default project permissions for any project in the organization. @@ -178,6 +180,7 @@ impl TeamMember { team_id: data.team_id.into(), user, role: data.role, + is_owner: data.is_owner, permissions: if override_permissions { None } else { diff --git a/src/routes/v2/teams.rs b/src/routes/v2/teams.rs index bdc4ea23..87e0a45e 100644 --- a/src/routes/v2/teams.rs +++ b/src/routes/v2/teams.rs @@ -1,6 +1,7 @@ use crate::database::redis::RedisPool; -use crate::models::teams::{OrganizationPermissions, ProjectPermissions, TeamId}; +use crate::models::teams::{OrganizationPermissions, ProjectPermissions, TeamId, TeamMember}; use crate::models::users::UserId; +use crate::models::v2::teams::LegacyTeamMember; use crate::queue::session::AuthQueue; use crate::routes::{v3, ApiError, v2_reroute}; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; @@ -34,7 +35,15 @@ pub async fn team_members_get_project( redis: web::Data, session_queue: web::Data, ) -> Result { - v3::teams::team_members_get_project(req, info, pool, redis, session_queue).await.or_else(v2_reroute::flatten_404_error) + let response = v3::teams::team_members_get_project(req, info, pool, redis, session_queue).await.or_else(v2_reroute::flatten_404_error)?; + // Convert response to V2 format + match v2_reroute::extract_ok_json::>(response).await { + Ok(members) => { + let members = members.into_iter().map(LegacyTeamMember::from).collect::>(); + Ok(HttpResponse::Ok().json(members)) + } + Err(response) => Ok(response), + } } #[get("{id}/members")] @@ -45,7 +54,15 @@ pub async fn team_members_get_organization( redis: web::Data, session_queue: web::Data, ) -> Result { - v3::teams::team_members_get_organization(req, info, pool, redis, session_queue).await.or_else(v2_reroute::flatten_404_error) + let response = v3::teams::team_members_get_organization(req, info, pool, redis, session_queue).await.or_else(v2_reroute::flatten_404_error)?; + // Convert response to V2 format + match v2_reroute::extract_ok_json::>(response).await { + Ok(members) => { + let members = members.into_iter().map(LegacyTeamMember::from).collect::>(); + Ok(HttpResponse::Ok().json(members)) + } + Err(response) => Ok(response), + } } // Returns all members of a team, but not necessarily those of a project-team's organization (unlike team_members_get_project) @@ -57,7 +74,15 @@ pub async fn team_members_get( redis: web::Data, session_queue: web::Data, ) -> Result { - v3::teams::team_members_get(req, info, pool, redis, session_queue).await.or_else(v2_reroute::flatten_404_error) + let response = v3::teams::team_members_get(req, info, pool, redis, session_queue).await.or_else(v2_reroute::flatten_404_error)?; + // Convert response to V2 format + match v2_reroute::extract_ok_json::>(response).await { + Ok(members) => { + let members = members.into_iter().map(LegacyTeamMember::from).collect::>(); + Ok(HttpResponse::Ok().json(members)) + } + Err(response) => Ok(response), + } } #[derive(Serialize, Deserialize)] @@ -73,14 +98,22 @@ pub async fn teams_get( redis: web::Data, session_queue: web::Data, ) -> Result { - v3::teams::teams_get( + let response = v3::teams::teams_get( req, web::Query(v3::teams::TeamIds { ids: ids.ids }), pool, redis, session_queue, ) - .await.or_else(v2_reroute::flatten_404_error) + .await.or_else(v2_reroute::flatten_404_error); + // Convert response to V2 format + match v2_reroute::extract_ok_json::>>(response?).await { + Ok(members) => { + let members = members.into_iter().map(|members| members.into_iter().map(LegacyTeamMember::from).collect::>()).collect::>(); + Ok(HttpResponse::Ok().json(members)) + } + Err(response) => Ok(response), + } } #[post("{id}/join")] diff --git a/src/routes/v2/users.rs b/src/routes/v2/users.rs index d2a71358..dbe0d70d 100644 --- a/src/routes/v2/users.rs +++ b/src/routes/v2/users.rs @@ -250,9 +250,7 @@ pub async fn user_notifications( redis: web::Data, session_queue: web::Data, ) -> Result { - println!("Gott notifications"); let response = v3::users::user_notifications(req, info, pool, redis, session_queue).await.or_else(v2_reroute::flatten_404_error)?; - // Convert response to V2 format match v2_reroute::extract_ok_json::>(response).await { Ok(notifications) => { diff --git a/src/routes/v3/organizations.rs b/src/routes/v3/organizations.rs index f3b60a6b..021db2a2 100644 --- a/src/routes/v3/organizations.rs +++ b/src/routes/v3/organizations.rs @@ -143,6 +143,7 @@ pub async fn organization_create( members: vec![team_item::TeamMemberBuilder { user_id: current_user.id.into(), role: crate::models::teams::OWNER_ROLE.to_owned(), + is_owner: true, permissions: ProjectPermissions::all(), organization_permissions: Some(OrganizationPermissions::all()), accepted: true, @@ -598,8 +599,7 @@ pub async fn organization_projects_add( // Require ownership of a project to add it to an organization if !current_user.role.is_admin() && !project_team_member - .role - .eq(crate::models::teams::OWNER_ROLE) + .is_owner { return Err(ApiError::CustomAuthentication( "You need to be an owner of a project to add it to an organization!".to_string(), diff --git a/src/routes/v3/project_creation.rs b/src/routes/v3/project_creation.rs index 65e83b78..fcfb0f57 100644 --- a/src/routes/v3/project_creation.rs +++ b/src/routes/v3/project_creation.rs @@ -642,6 +642,7 @@ async fn project_create_inner( members: vec![models::team_item::TeamMemberBuilder { user_id: current_user.id.into(), role: crate::models::teams::OWNER_ROLE.to_owned(), + is_owner: true, // Allow all permissions for project creator, even if attached to a project permissions: ProjectPermissions::all(), organization_permissions: None, diff --git a/src/routes/v3/teams.rs b/src/routes/v3/teams.rs index b4d4ce52..008be649 100644 --- a/src/routes/v3/teams.rs +++ b/src/routes/v3/teams.rs @@ -343,6 +343,7 @@ pub async fn join_team( Some(true), None, None, + None, &mut transaction, ) .await?; @@ -473,12 +474,6 @@ pub async fn add_team_member( } } - if new_member.role == crate::models::teams::OWNER_ROLE { - return Err(ApiError::InvalidInput( - "The `Owner` role is restricted to one person".to_string(), - )); - } - if new_member.payouts_split < Decimal::ZERO || new_member.payouts_split > Decimal::from(5000) { return Err(ApiError::InvalidInput( "Payouts split must be between 0 and 5000!".to_string(), @@ -509,6 +504,7 @@ pub async fn add_team_member( team_id, user_id: new_member.user_id.into(), role: new_member.role.clone(), + is_owner: false, // Cannot just create an owner permissions: new_member.permissions, organization_permissions: new_member.organization_permissions, accepted: false, @@ -597,11 +593,10 @@ pub async fn edit_team_member( let mut transaction = pool.begin().await?; - if &*edit_member_db.role == crate::models::teams::OWNER_ROLE - && (edit_member.role.is_some() || edit_member.permissions.is_some()) + if edit_member_db.is_owner && edit_member.permissions.is_some() { return Err(ApiError::InvalidInput( - "The owner's permission and role of a team cannot be edited".to_string(), + "The owner's permission's in a team cannot be edited".to_string(), )); } @@ -682,12 +677,6 @@ pub async fn edit_team_member( } } - if edit_member.role.as_deref() == Some(crate::models::teams::OWNER_ROLE) { - return Err(ApiError::InvalidInput( - "The `Owner` role is restricted to one person".to_string(), - )); - } - TeamMember::edit_team_member( id, user_id, @@ -697,6 +686,7 @@ pub async fn edit_team_member( None, edit_member.payouts_split, edit_member.ordering, + None, &mut transaction, ) .await?; @@ -757,7 +747,7 @@ pub async fn transfer_ownership( ) })?; - if member.role != crate::models::teams::OWNER_ROLE { + if !member.is_owner { return Err(ApiError::CustomAuthentication( "You don't have permission to edit the ownership of this team".to_string(), )); @@ -778,15 +768,17 @@ pub async fn transfer_ownership( let mut transaction = pool.begin().await?; + // The following are the only places new_is_owner is modified. TeamMember::edit_team_member( id.into(), current_user.id.into(), None, None, - Some(crate::models::teams::DEFAULT_ROLE.to_string()), None, None, None, + None, + Some(false), &mut transaction, ) .await?; @@ -796,10 +788,11 @@ pub async fn transfer_ownership( new_owner.user_id.into(), Some(ProjectPermissions::all()), Some(OrganizationPermissions::all()), - Some(crate::models::teams::OWNER_ROLE.to_string()), None, None, None, + None, + Some(true), &mut transaction, ) .await?; @@ -840,7 +833,7 @@ pub async fn remove_team_member( let delete_member = TeamMember::get_from_user_id_pending(id, user_id, &**pool).await?; if let Some(delete_member) = delete_member { - if delete_member.role == crate::models::teams::OWNER_ROLE { + if delete_member.is_owner { // The owner cannot be removed from a team return Err(ApiError::CustomAuthentication( "The owner can't be removed from a team".to_string(), diff --git a/src/search/indexing/local_import.rs b/src/search/indexing/local_import.rs index e6b287cb..d57bc1a7 100644 --- a/src/search/indexing/local_import.rs +++ b/src/search/indexing/local_import.rs @@ -72,7 +72,7 @@ pub async fn index_local( LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id LEFT JOIN games g ON lptg.game_id = g.id LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id - INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE + INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.is_owner = TRUE AND tm.accepted = TRUE INNER JOIN users u ON tm.user_id = u.id LEFT OUTER JOIN version_fields vf on v.id = vf.version_id LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id @@ -83,7 +83,6 @@ pub async fn index_local( ", &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::>(), &*crate::models::projects::ProjectStatus::iterator().filter(|x| x.is_searchable()).map(|x| x.to_string()).collect::>(), - crate::models::teams::OWNER_ROLE, ) .fetch_many(&pool) .try_filter_map(|e| { diff --git a/src/util/webhook.rs b/src/util/webhook.rs index 8bc0ddca..c9f1b54e 100644 --- a/src/util/webhook.rs +++ b/src/util/webhook.rs @@ -135,7 +135,7 @@ pub async fn send_discord_webhook( LEFT JOIN loaders_project_types_games lptg ON lptg.loader_id = lo.id AND lptg.project_type_id = pt.id LEFT JOIN games g ON lptg.game_id = g.id LEFT OUTER JOIN mods_gallery mg ON mg.mod_id = m.id - INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.role = $3 AND tm.accepted = TRUE + INNER JOIN team_members tm ON tm.team_id = m.team_id AND tm.is_owner = TRUE AND tm.accepted = TRUE INNER JOIN users u ON tm.user_id = u.id LEFT OUTER JOIN version_fields vf on v.id = vf.version_id LEFT OUTER JOIN loader_fields lf on vf.field_id = lf.id @@ -146,7 +146,6 @@ pub async fn send_discord_webhook( ", project_id.0 as i64, &*crate::models::projects::VersionStatus::iterator().filter(|x| x.is_hidden()).map(|x| x.to_string()).collect::>(), - crate::models::teams::OWNER_ROLE, ) .fetch_optional(pool) .await?; diff --git a/tests/common/api_v2/team.rs b/tests/common/api_v2/team.rs index 88584161..c5ba8840 100644 --- a/tests/common/api_v2/team.rs +++ b/tests/common/api_v2/team.rs @@ -1,7 +1,7 @@ use actix_http::StatusCode; use actix_web::{dev::ServiceResponse, test}; use async_trait::async_trait; -use labrinth::models::{teams::{OrganizationPermissions, ProjectPermissions}, v2::notifications::LegacyNotification}; +use labrinth::models::{teams::{OrganizationPermissions, ProjectPermissions}, v2::{notifications::LegacyNotification, teams::LegacyTeamMember}}; use serde_json::json; use crate::common::{ @@ -14,6 +14,29 @@ use crate::common::{ use super::ApiV2; +impl ApiV2 { + 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 get_team_members_deserialized( + &self, + team_id: &str, + pat: &str, + ) -> Vec { + let resp = self.get_team_members(team_id, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } + +} + #[async_trait(?Send)] impl ApiTeams for ApiV2 { async fn get_team_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { @@ -139,7 +162,6 @@ impl ApiTeams for ApiV2 { user_id: &str, pat: &str, ) -> Vec { - println!("V2 deserializing"); let resp = self.get_user_notifications(user_id, pat).await; assert_status(&resp, StatusCode::OK); // First, deserialize to the non-common format (to test the response is valid for this api version) diff --git a/tests/common/api_v3/project.rs b/tests/common/api_v3/project.rs index e01520ff..fcb95c8c 100644 --- a/tests/common/api_v3/project.rs +++ b/tests/common/api_v3/project.rs @@ -229,7 +229,6 @@ impl ApiProject for ApiV3 { .to_request(); let resp = self.call(req).await; let status = resp.status(); - println!("Body: {:?}", resp.response().body()); assert_eq!(status, 200); test::read_body_json(resp).await } diff --git a/tests/common/api_v3/team.rs b/tests/common/api_v3/team.rs index 7d739536..47a36f24 100644 --- a/tests/common/api_v3/team.rs +++ b/tests/common/api_v3/team.rs @@ -14,6 +14,28 @@ use crate::common::{ use super::ApiV3; +impl ApiV3 { + 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 get_team_members_deserialized( + &self, + team_id: &str, + pat: &str, + ) -> Vec { + let resp = self.get_team_members(team_id, pat).await; + assert_eq!(resp.status(), 200); + test::read_body_json(resp).await + } +} + #[async_trait(?Send)] impl ApiTeams for ApiV3 { async fn get_team_members(&self, id_or_title: &str, pat: &str) -> ServiceResponse { @@ -144,8 +166,6 @@ impl ApiTeams for ApiV3 { ) -> Vec { let resp = self.get_user_notifications(user_id, pat).await; assert_status(&resp, StatusCode::OK); - println!("v3 deserializing"); - // First, deserialize to the non-common format (to test the response is valid for this api version) let v: Vec = test::read_body_json(resp).await; // Then, deserialize to the common format diff --git a/tests/common/dummy_data.rs b/tests/common/dummy_data.rs index 5e6f5d30..14af45f7 100644 --- a/tests/common/dummy_data.rs +++ b/tests/common/dummy_data.rs @@ -380,7 +380,6 @@ pub async fn add_project_beta(api: &ApiV3) -> (Project, Version) { .set_multipart(vec![json_segment.clone(), file_segment.clone()]) .to_request(); let resp = api.call(req).await; - println!("{:?}", resp.response().body()); assert_eq!(resp.status(), 200); get_project_beta(api).await diff --git a/tests/organizations.rs b/tests/organizations.rs index f6fa13d5..557c90a8 100644 --- a/tests/organizations.rs +++ b/tests/organizations.rs @@ -71,7 +71,7 @@ async fn create_organization() { // Get created team let members = api - .get_organization_members_deserialized_common("theta", USER_USER_PAT) + .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 @@ -81,6 +81,7 @@ async fn create_organization() { Some(OrganizationPermissions::all()) ); assert_eq!(members[0].role, "Owner"); + assert_eq!(members[0].is_owner, true); }) .await; } diff --git a/tests/project.rs b/tests/project.rs index db9f2e97..7a1ffc74 100644 --- a/tests/project.rs +++ b/tests/project.rs @@ -172,7 +172,6 @@ async fn test_add_remove_project() { }, USER_USER_PAT).await; let status = resp.status(); - println!("Body: {:?}", resp.response().body()); assert_eq!(status, 200); // Get the project we just made, and confirm that it's correct @@ -482,11 +481,15 @@ pub async fn test_bulk_edit_categories() { } #[actix_rt::test] -async fn permissions_patch_project() { - with_test_environment_all(Some(8), |test_env| async move { +async fn permissions_patch_project_v3() { + with_test_environment(Some(8), |test_env: TestEnvironment| async move { 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; + // TODO: This should be a separate test from v3 + // - only a couple of these fields are v3-specific + // once we have permissions/scope tests setup to not just take closures, we can split this up + // For each permission covered by EDIT_DETAILS, ensure the permission is required let edit_details = ProjectPermissions::EDIT_DETAILS; let test_pairs = [ @@ -496,18 +499,10 @@ async fn permissions_patch_project() { ("description", json!("randomdescription")), ("categories", json!(["combat", "economy"])), ("additional_categories", json!(["decoration"])), - ("issues_url", json!("https://issues.com")), - ("source_url", json!("https://source.com")), - ("wiki_url", json!("https://wiki.com")), - ( - "donation_urls", - json!([{ - "id": "paypal", - "platform": "Paypal", - "url": "https://paypal.com" - }]), - ), - ("discord_url", json!("https://discord.com")), + ("links", json!({ + "issues": "https://issues.com", + "source": "https://source.com", + })), ("license_id", json!("MIT")), ]; @@ -576,7 +571,7 @@ async fn permissions_patch_project() { test::TestRequest::patch() .uri(&format!("/v3/project/{}", ctx.project_id.unwrap())) .set_json(json!({ - "body": "new body!", + "description": "new description!", })) }; PermissionsTest::new(&test_env) diff --git a/tests/scopes.rs b/tests/scopes.rs index 3981bcc6..3db4037f 100644 --- a/tests/scopes.rs +++ b/tests/scopes.rs @@ -3,8 +3,9 @@ use bytes::Bytes; use chrono::{Duration, Utc}; use common::api_common::ApiProject; +use common::api_v3::ApiV3; use common::dummy_data::TestFile; -use common::environment::with_test_environment_all; +use common::environment::{with_test_environment_all, TestEnvironment, with_test_environment}; use common::{database::*, scopes::ScopeTest}; use labrinth::models::pats::Scopes; use labrinth::util::actix::{AppendsMultipart, MultipartSegment, MultipartSegmentData}; @@ -202,8 +203,9 @@ pub async fn notifications_scopes() { // Project version creation scopes #[actix_rt::test] -pub async fn project_version_create_scopes() { - with_test_environment_all(None, |test_env| async move { +pub async fn project_version_create_scopes_v3() { + with_test_environment(None, |test_env : TestEnvironment| async move { + // TODO: If possible, find a way to use generic api functions with the Permissions/Scopes test, then this can be recombined with the V2 version of this test let api = &test_env.api; // Create project diff --git a/tests/teams.rs b/tests/teams.rs index 929d0bec..ea3a8c82 100644 --- a/tests/teams.rs +++ b/tests/teams.rs @@ -1,7 +1,8 @@ -use crate::common::database::*; +use crate::common::{database::*, api_common::ApiTeams}; use actix_web::test; -use common::environment::with_test_environment_all; +use common::{environment::{with_test_environment_all, with_test_environment, TestEnvironment}, api_v3::ApiV3}; use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; +use rust_decimal::Decimal; use serde_json::json; mod common; @@ -209,152 +210,69 @@ async fn test_get_team_project_orgs() { async fn test_patch_project_team_member() { // Test setup and dummy data with_test_environment_all(None, |test_env| async move { + let api = &test_env.api; + 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!("/v3/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; + let resp = api.edit_team_member(alpha_team_id, USER_USER_ID, json!({}), ADMIN_USER_PAT).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!("/v3/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!("/v3/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; - + // As a non-owner with full permissions, attempt to edit the owner's permissions + let resp = api.edit_team_member(alpha_team_id, USER_USER_ID, json!({ + "permissions": 0 + }), ADMIN_USER_PAT).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!("/v3/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; - + let resp = api.edit_team_member(alpha_team_id, USER_USER_ID, json!({ + "organization_permissions": 0 + }), USER_USER_PAT).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!("/v3/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; + let resp = api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, + Some(ProjectPermissions::EDIT_MEMBER | ProjectPermissions::EDIT_BODY), + None, USER_USER_PAT).await; assert_eq!(resp.status(), 204); // accept - let req = test::TestRequest::post() - .uri(&format!("/v3/team/{alpha_team_id}/join")) - .append_header(("Authorization", FRIEND_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; + let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await; assert_eq!(resp.status(), 204); // try to add permissions - let req = test::TestRequest::patch() - .uri(&format!("/v3/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!( - "/v3/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; + let resp = api.edit_team_member(alpha_team_id, FRIEND_USER_ID, json!({ + "permissions": (ProjectPermissions::EDIT_MEMBER | ProjectPermissions::EDIT_DETAILS).bits() + }), FRIEND_USER_PAT).await; // should this be friend_user_pat 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!( - "/v3/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; - + let resp = api.edit_team_member(alpha_team_id, FRIEND_USER_ID, json!({ + "payouts_split": payout + }), USER_USER_PAT).await; assert_eq!(resp.status(), 400); } // Successful patch - let req = test::TestRequest::patch() - .uri(&format!( - "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" - )) - .append_header(("Authorization", FRIEND_USER_PAT)) - .set_json(json!({ + let resp = api.edit_team_member(alpha_team_id, FRIEND_USER_ID, json!({ "payouts_split": 51, "permissions": ProjectPermissions::EDIT_MEMBER.bits(), // reduces permissions - "role": "member", + "role": "membe2r", "ordering": 5 - })) - .to_request(); - let resp = test_env.call(req).await; + }), FRIEND_USER_PAT).await; assert_eq!(resp.status(), 204); // Check results - let req = test::TestRequest::get() - .uri(&format!("/v3/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); - + let members = api.get_team_members_deserialized_common(alpha_team_id, FRIEND_USER_PAT).await; + let member = members.iter().find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64).unwrap(); + assert_eq!(member.payouts_split, Decimal::from_f64_retain(51.0)); + assert_eq!(member.permissions.unwrap(), ProjectPermissions::EDIT_MEMBER); + assert_eq!(member.role, "membe2r"); + assert_eq!(member.ordering, 5); }).await; } @@ -374,17 +292,7 @@ async fn test_patch_organization_team_member() { 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!("/v3/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); - + // As a non-owner with full permissions, attempt to edit the owner's permissions let req = test::TestRequest::patch() .uri(&format!("/v3/team/{zeta_team_id}/members/{USER_USER_ID}")) .append_header(("Authorization", ADMIN_USER_PAT)) @@ -429,18 +337,6 @@ async fn test_patch_organization_team_member() { assert_eq!(resp.status(), 400); - // Cannot set a user to Owner - let req = test::TestRequest::patch() - .uri(&format!("/v3/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() @@ -462,7 +358,7 @@ async fn test_patch_organization_team_member() { "payouts_split": 51, "organization_permissions": (OrganizationPermissions::EDIT_MEMBER).bits(), // reduces permissions "permissions": (ProjectPermissions::EDIT_MEMBER).bits(), - "role": "member", + "role": "very-cool-member", "ordering": 5 })) .to_request(); @@ -493,7 +389,7 @@ async fn test_patch_organization_team_member() { member["permissions"], ProjectPermissions::EDIT_MEMBER.bits() ); - assert_eq!(member["role"], "member"); + assert_eq!(member["role"], "very-cool-member"); assert_eq!(member["ordering"], 5); }).await; @@ -501,104 +397,65 @@ async fn test_patch_organization_team_member() { // trasnfer ownership (requires being owner, etc) #[actix_rt::test] -async fn transfer_ownership() { +async fn transfer_ownership_v3() { // Test setup and dummy data - with_test_environment_all(None, |test_env| async move { + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + 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!("/v3/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; + let resp = api.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT).await; + assert_eq!(resp.status(), 400); + let resp = api.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, FRIEND_USER_PAT).await; assert_eq!(resp.status(), 401); // first, invite friend - let req = test::TestRequest::post() - .uri(&format!("/v3/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; + let resp = api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT).await; assert_eq!(resp.status(), 204); + // still cannot set friend as owner (not accepted) + let resp = api.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT).await; + assert_eq!(resp.status(), 400); + // accept - let req = test::TestRequest::post() - .uri(&format!("/v3/team/{alpha_team_id}/join")) - .append_header(("Authorization", FRIEND_USER_PAT)) - .to_request(); - let resp = test_env.call(req).await; + let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await; assert_eq!(resp.status(), 204); - // Cannot set ourselves as owner - let req = test::TestRequest::patch() - .uri(&format!("/v3/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; + // Cannot set ourselves as owner if we are not owner + let resp = api.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, FRIEND_USER_PAT).await; assert_eq!(resp.status(), 401); // Can set friend as owner - let req = test::TestRequest::patch() - .uri(&format!("/v3/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; + let resp = api.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT).await; assert_eq!(resp.status(), 204); // Check - let req = test::TestRequest::get() - .uri(&format!("/v3/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()); + let members = api.get_team_members_deserialized(alpha_team_id, USER_USER_PAT).await; + let friend_member = members.iter().find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64).unwrap(); + assert_eq!(friend_member.role, "Member"); // her role does not actually change, but is_owner is set to true + assert!(friend_member.is_owner); + assert_eq!(friend_member.permissions.unwrap(), ProjectPermissions::all()); - // Confirm that user, a user who still has full permissions, cannot then remove the owner - let req = test::TestRequest::delete() - .uri(&format!( - "/v3/team/{alpha_team_id}/members/{FRIEND_USER_ID}" - )) - .append_header(("Authorization", USER_USER_PAT)) - .to_request(); + let user_member = members.iter().find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64).unwrap(); + assert_eq!(user_member.role, "Owner"); // We are the 'owner', but we are not actually the owner! + assert!(!user_member.is_owner); + assert_eq!(user_member.permissions.unwrap(), ProjectPermissions::all()); - let resp = test_env.call(req).await; + // Confirm that user, a user who still has full permissions, cannot then remove the owner + let resp = api.remove_from_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT).await; assert_eq!(resp.status(), 401); + + // V3 only- confirm the owner can change their role without losing ownership + let resp = api.edit_team_member(alpha_team_id, FRIEND_USER_ID, json!({ + "role": "Member" + }), FRIEND_USER_PAT).await; + assert_eq!(resp.status(), 204); + + let members = api.get_team_members_deserialized(alpha_team_id, USER_USER_PAT).await; + let friend_member = members.iter().find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64).unwrap(); + assert_eq!(friend_member.role, "Member"); + assert!(friend_member.is_owner); }) .await; } diff --git a/tests/v2/project.rs b/tests/v2/project.rs index 4a518a02..a7ebbe1b 100644 --- a/tests/v2/project.rs +++ b/tests/v2/project.rs @@ -1,12 +1,13 @@ use crate::common::{ api_common::ApiProject, api_v2::ApiV2, - database::{FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_PAT}, + database::{FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_PAT, generate_random_name}, dummy_data::TestFile, environment::{with_test_environment, TestEnvironment}, permissions::{PermissionsTest, PermissionsTestContext}, }; use actix_web::test; +use futures::StreamExt; use itertools::Itertools; use labrinth::{ database::models::project_item::PROJECTS_SLUGS_NAMESPACE, @@ -397,3 +398,67 @@ pub async fn test_patch_v2() { }).await; } +#[actix_rt::test] +async fn permissions_patch_project_v2() { + with_test_environment(Some(8), |test_env: TestEnvironment| async move { + // TODO: This only includes v2 ones (as it should. See v3) + // For each permission covered by EDIT_DETAILS, ensure the permission is required + let edit_details = ProjectPermissions::EDIT_DETAILS; + let test_pairs = [ + ("description", json!("description")), + ("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")), + ]; + + 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 + .into_iter(); + } + }) + .buffer_unordered(4) + .collect::>() + .await; + + // 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!", // new body + })) + }; + PermissionsTest::new(&test_env) + .simple_project_permissions_test(edit_body, req_gen) + .await + .unwrap(); + }) + .await; +} diff --git a/tests/v2/scopes.rs b/tests/v2/scopes.rs index afac8628..fd635acc 100644 --- a/tests/v2/scopes.rs +++ b/tests/v2/scopes.rs @@ -89,3 +89,83 @@ pub async fn project_version_create_scopes() { }) .await; } + +#[actix_rt::test] +pub async fn project_version_create_scopes_v2() { + with_test_environment(None, |test_env : TestEnvironment| async move { + // TODO: If possible, find a way to use generic api functions with the Permissions/Scopes test, then this can be recombined with the V2 version of this test + let api = &test_env.api; + + // Create project + let create_project = Scopes::PROJECT_CREATE; + let json_data = api.get_public_project_creation_data_json("demo", Some(&TestFile::BasicMod)).await; + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + }; + let file_segment = MultipartSegment { + name: "basic-mod.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod.jar").to_vec(), + ), + }; + + let req_gen = || { + test::TestRequest::post() + .uri("/v2/project") + .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + }; + let (_, success) = ScopeTest::new(&test_env) + .test(req_gen, create_project) + .await + .unwrap(); + let project_id = success["id"].as_str().unwrap(); + + // Add version to project + let create_version = Scopes::VERSION_CREATE; + let json_data = json!( + { + "project_id": project_id, + "file_parts": ["basic-mod-different.jar"], + "version_number": "1.2.3.4", + "version_title": "start", + "dependencies": [], + "game_versions": ["1.20.1"] , + "client_side": "required", + "server_side": "optional", + "release_channel": "release", + "loaders": ["fabric"], + "featured": true + } + ); + let json_segment = MultipartSegment { + name: "data".to_string(), + filename: None, + content_type: Some("application/json".to_string()), + data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), + }; + let file_segment = MultipartSegment { + name: "basic-mod-different.jar".to_string(), + filename: Some("basic-mod.jar".to_string()), + content_type: Some("application/java-archive".to_string()), + data: MultipartSegmentData::Binary( + include_bytes!("../../tests/files/basic-mod-different.jar").to_vec(), + ), + }; + + let req_gen = || { + test::TestRequest::post() + .uri("/v2/version") + .set_multipart(vec![json_segment.clone(), file_segment.clone()]) + }; + ScopeTest::new(&test_env) + .test(req_gen, create_version) + .await + .unwrap(); + }) + .await; +} diff --git a/tests/v2/teams.rs b/tests/v2/teams.rs new file mode 100644 index 00000000..37fae4d2 --- /dev/null +++ b/tests/v2/teams.rs @@ -0,0 +1,63 @@ +use labrinth::models::teams::ProjectPermissions; +use serde_json::json; + +use crate::common::{environment::{with_test_environment, TestEnvironment}, database::{USER_USER_PAT,FRIEND_USER_ID, FRIEND_USER_PAT, FRIEND_USER_ID_PARSED, USER_USER_ID_PARSED}, api_v2::ApiV2, api_common::ApiTeams}; + +// trasnfer ownership (requires being owner, etc) +#[actix_rt::test] +async fn transfer_ownership_v2() { + // Test setup and dummy data + with_test_environment(None, |test_env: TestEnvironment| async move { + let api = &test_env.api; + + let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id; + + // Cannot set friend as owner (not a member) + let resp = api.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT).await; + assert_eq!(resp.status(), 400); + + // first, invite friend + let resp = api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT).await; + assert_eq!(resp.status(), 204); + + // still cannot set friend as owner (not accepted) + let resp = api.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT).await; + assert_eq!(resp.status(), 400); + + // accept + let resp = api.join_team(alpha_team_id, FRIEND_USER_PAT).await; + assert_eq!(resp.status(), 204); + + // Cannot set ourselves as owner if we are not owner + let resp = api.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, FRIEND_USER_PAT).await; + assert_eq!(resp.status(), 401); + + // Can set friend as owner + let resp = api.transfer_team_ownership(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT).await; + assert_eq!(resp.status(), 204); + + // Check + let members = api.get_team_members_deserialized(alpha_team_id, USER_USER_PAT).await; + let friend_member = members.iter().find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64).unwrap(); + assert_eq!(friend_member.role, "Owner"); + assert_eq!(friend_member.permissions.unwrap(), ProjectPermissions::all()); + + let user_member = members.iter().find(|x| x.user.id.0 == USER_USER_ID_PARSED as u64).unwrap(); + assert_eq!(user_member.role, "Member"); + assert_eq!(user_member.permissions.unwrap(), ProjectPermissions::all()); + + // Confirm that user, a user who still has full permissions, cannot then remove the owner + let resp = api.remove_from_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT).await; + assert_eq!(resp.status(), 401); + + // V2 only- confirm the owner changing the role to member does nothing + let resp = api.edit_team_member(alpha_team_id, FRIEND_USER_ID, json!({ + "role": "Member" + }), FRIEND_USER_PAT).await; + assert_eq!(resp.status(), 204); + let members = api.get_team_members_deserialized(alpha_team_id, USER_USER_PAT).await; + let friend_member = members.iter().find(|x| x.user.id.0 == FRIEND_USER_ID_PARSED as u64).unwrap(); + assert_eq!(friend_member.role, "Owner"); + }) + .await; +} \ No newline at end of file diff --git a/tests/v2_tests.rs b/tests/v2_tests.rs index 26d26d15..7b945822 100644 --- a/tests/v2_tests.rs +++ b/tests/v2_tests.rs @@ -13,5 +13,6 @@ mod v2 { mod scopes; mod search; mod tags; + mod teams; mod version; }