Organization ownership (#796)

* organization changes

* changes

* fixes failing test

* version changes

* removed printlns

* add_team_member comes pre-accepted

* no notification on force accept

* fixes tests

* merge fixes
This commit is contained in:
Wyatt Verchere
2023-12-20 14:27:57 -08:00
committed by GitHub
parent 60c535e861
commit f7b4b782bf
31 changed files with 910 additions and 125 deletions

View File

@@ -3,7 +3,7 @@ use actix_web::{
test::{self, TestRequest},
};
use bytes::Bytes;
use labrinth::models::{organizations::Organization, v3::projects::Project};
use labrinth::models::{organizations::Organization, users::UserId, v3::projects::Project};
use serde_json::json;
use crate::common::api_common::{request_data::ImageData, Api, AppendsOptionalPat};
@@ -162,12 +162,16 @@ impl ApiV3 {
&self,
id_or_title: &str,
project_id_or_slug: &str,
new_owner_user_id: UserId,
pat: Option<&str>,
) -> ServiceResponse {
let req = test::TestRequest::delete()
.uri(&format!(
"/v3/organization/{id_or_title}/projects/{project_id_or_slug}"
))
.set_json(json!({
"new_owner": new_owner_user_id,
}))
.append_pat(pat)
.to_request();

View File

@@ -37,6 +37,16 @@ impl ApiV3 {
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
pub async fn get_project_members_deserialized(
&self,
project_id: &str,
pat: Option<&str>,
) -> Vec<TeamMember> {
let resp = self.get_project_members(project_id, pat).await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
}
#[async_trait(?Send)]

View File

@@ -1051,8 +1051,10 @@ async fn add_user_to_team(
assert!(resp.status().is_success());
// Accept invitation
let resp = setup_api.join_team(team_id, user_pat).await;
assert!(resp.status().is_success());
setup_api.join_team(team_id, user_pat).await;
// This does not check if the join request was successful,
// as the join is not always needed- an org project + in-org invite
// will automatically go through.
}
async fn modify_user_team_permissions(

View File

@@ -1,7 +1,10 @@
use crate::common::{
api_common::ApiTeams,
database::{generate_random_name, ADMIN_USER_PAT, MOD_USER_ID, MOD_USER_PAT, USER_USER_ID},
dummy_data::DummyImage,
api_common::{ApiProject, ApiTeams},
database::{
generate_random_name, ADMIN_USER_PAT, ENEMY_USER_ID_PARSED, ENEMY_USER_PAT,
FRIEND_USER_ID_PARSED, MOD_USER_ID, MOD_USER_PAT, USER_USER_ID, USER_USER_ID_PARSED,
},
dummy_data::{DummyImage, DummyOrganizationZeta, DummyProjectAlpha, DummyProjectBeta},
};
use common::{
api_v3::ApiV3,
@@ -9,7 +12,10 @@ use common::{
environment::{with_test_environment, with_test_environment_all, TestEnvironment},
permissions::{PermissionsTest, PermissionsTestContext},
};
use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions};
use labrinth::models::{
teams::{OrganizationPermissions, ProjectPermissions},
users::UserId,
};
use serde_json::json;
mod common;
@@ -249,7 +255,12 @@ async fn add_remove_organization_projects() {
// Remove project from organization
let resp = test_env
.api
.organization_remove_project(zeta_organization_id, alpha, USER_USER_PAT)
.organization_remove_project(
zeta_organization_id,
alpha,
UserId(USER_USER_ID_PARSED as u64),
USER_USER_PAT,
)
.await;
assert_eq!(resp.status(), 200);
@@ -264,6 +275,357 @@ async fn add_remove_organization_projects() {
.await;
}
// Like above, but specifically regarding ownership transferring
#[actix_rt::test]
async fn add_remove_organization_project_ownership_to_user() {
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
let DummyProjectAlpha {
project_id: alpha_project_id,
team_id: alpha_team_id,
..
} = &test_env.dummy.project_alpha;
let DummyProjectBeta {
project_id: beta_project_id,
team_id: beta_team_id,
..
} = &test_env.dummy.project_beta;
let DummyOrganizationZeta {
organization_id: zeta_organization_id,
team_id: zeta_team_id,
..
} = &test_env.dummy.organization_zeta;
// Add friend to alpha, beta, and zeta
for (team, organization) in [
(alpha_team_id, false),
(beta_team_id, false),
(zeta_team_id, true),
] {
let org_permissions = if organization {
Some(OrganizationPermissions::all())
} else {
None
};
let resp = test_env
.api
.add_user_to_team(
team,
FRIEND_USER_ID,
Some(ProjectPermissions::all()),
org_permissions,
USER_USER_PAT,
)
.await;
assert_eq!(resp.status(), 204);
// Accept invites
let resp = test_env.api.join_team(team, FRIEND_USER_PAT).await;
assert_eq!(resp.status(), 204);
}
// For each team, confirm there are two members, but only one owner of the project, and it is USER_USER_ID
for team in [alpha_team_id, beta_team_id, zeta_team_id] {
let members = test_env
.api
.get_team_members_deserialized(team, USER_USER_PAT)
.await;
assert_eq!(members.len(), 2);
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
assert_eq!(user_member.len(), 1);
assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID);
}
// Transfer ownership of beta project to FRIEND
let resp = test_env
.api
.transfer_team_ownership(beta_team_id, FRIEND_USER_ID, USER_USER_PAT)
.await;
assert_eq!(resp.status(), 204);
// Confirm there are still two users, but now FRIEND_USER_ID is the owner
let members = test_env
.api
.get_team_members_deserialized(beta_team_id, USER_USER_PAT)
.await;
assert_eq!(members.len(), 2);
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
assert_eq!(user_member.len(), 1);
assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID);
// Add alpha, beta to zeta organization
for (project_id, pat) in [
(alpha_project_id, USER_USER_PAT),
(beta_project_id, FRIEND_USER_PAT),
] {
let resp = test_env
.api
.organization_add_project(zeta_organization_id, project_id, pat)
.await;
assert_eq!(resp.status(), 200);
// Get and confirm it has been added
let project = test_env.api.get_project_deserialized(project_id, pat).await;
assert_eq!(
&project.organization.unwrap().to_string(),
zeta_organization_id
);
}
// Both alpha and beta project should have:
// - 1 member, FRIEND_USER_ID
// - No owner.
// -> For alpha, user was removed as owner when it was added to the organization
// -> For beta, user was removed as owner when ownership was transferred to friend
// then friend was removed as owner when it was added to the organization
// -> In both cases, user was removed entirely as a team_member as it is now the owner of the organization
for team_id in [alpha_team_id, beta_team_id] {
let members = test_env
.api
.get_team_members_deserialized(team_id, USER_USER_PAT)
.await;
assert_eq!(members.len(), 1);
assert_eq!(members[0].user.id.to_string(), FRIEND_USER_ID);
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
assert_eq!(user_member.len(), 0);
}
// Transfer ownership of zeta organization to FRIEND
let resp = test_env
.api
.transfer_team_ownership(zeta_team_id, FRIEND_USER_ID, USER_USER_PAT)
.await;
assert_eq!(resp.status(), 204);
// Confirm there are no members of the alpha project OR the beta project
// - Friend was removed as a member of these projects when ownership was transferred to them
for team_id in [alpha_team_id, beta_team_id] {
let members = test_env
.api
.get_team_members_deserialized(team_id, USER_USER_PAT)
.await;
assert!(members.is_empty());
}
// As user, cannot add friend to alpha project, as they are the org owner
let resp = test_env
.api
.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
.await;
assert_eq!(resp.status(), 400);
// As friend, can add user to alpha project, as they are not the org owner
let resp = test_env
.api
.add_user_to_team(alpha_team_id, USER_USER_ID, None, None, FRIEND_USER_PAT)
.await;
assert_eq!(resp.status(), 204);
// At this point, friend owns the org
// Alpha member has user as a member, but not as an owner
// Neither project has an owner, as they are owned by the org
// Remove project from organization with a user that is not an organization member
// This should fail as we cannot give a project to a user that is not a member of the organization
let resp = test_env
.api
.organization_remove_project(
zeta_organization_id,
alpha_project_id,
UserId(ENEMY_USER_ID_PARSED as u64),
USER_USER_PAT,
)
.await;
assert_eq!(resp.status(), 400);
// Remove project from organization with a user that is an organization member, and a project member
// This should succeed
let resp = test_env
.api
.organization_remove_project(
zeta_organization_id,
alpha_project_id,
UserId(USER_USER_ID_PARSED as u64),
USER_USER_PAT,
)
.await;
assert_eq!(resp.status(), 200);
// Remove project from organization with a user that is an organization member, but not a project member
// This should succeed
let resp = test_env
.api
.organization_remove_project(
zeta_organization_id,
beta_project_id,
UserId(USER_USER_ID_PARSED as u64),
USER_USER_PAT,
)
.await;
assert_eq!(resp.status(), 200);
// For each of alpha and beta, confirm:
// - There is one member of each project, the owner, USER_USER_ID
// - They no longer have an attached organization
for team_id in [alpha_team_id, beta_team_id] {
let members = test_env
.api
.get_team_members_deserialized(team_id, USER_USER_PAT)
.await;
assert_eq!(members.len(), 1);
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
assert_eq!(user_member.len(), 1);
assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID);
}
for project_id in [alpha_project_id, beta_project_id] {
let project = test_env
.api
.get_project_deserialized(project_id, USER_USER_PAT)
.await;
assert!(project.organization.is_none());
}
})
.await;
}
#[actix_rt::test]
async fn delete_organization_means_all_projects_to_org_owner() {
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
let DummyProjectAlpha {
project_id: alpha_project_id,
team_id: alpha_team_id,
..
} = &test_env.dummy.project_alpha;
let DummyProjectBeta {
project_id: beta_project_id,
team_id: beta_team_id,
..
} = &test_env.dummy.project_beta;
let DummyOrganizationZeta {
organization_id: zeta_organization_id,
team_id: zeta_team_id,
..
} = &test_env.dummy.organization_zeta;
// Create random project from enemy, ensure it wont get affected
let (enemy_project, _) = test_env
.api
.add_public_project("enemy_project", None, None, ENEMY_USER_PAT)
.await;
// Add FRIEND
let resp = test_env
.api
.add_user_to_team(zeta_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
.await;
assert_eq!(resp.status(), 204);
// Accept invite
let resp = test_env.api.join_team(zeta_team_id, FRIEND_USER_PAT).await;
assert_eq!(resp.status(), 204);
// Confirm there is only one owner of the project, and it is USER_USER_ID
let members = test_env
.api
.get_team_members_deserialized(alpha_team_id, USER_USER_PAT)
.await;
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
assert_eq!(user_member.len(), 1);
assert_eq!(user_member[0].user.id.to_string(), USER_USER_ID);
// Add alpha to zeta organization
let resp = test_env
.api
.organization_add_project(zeta_organization_id, alpha_project_id, USER_USER_PAT)
.await;
assert_eq!(resp.status(), 200);
// Add beta to zeta organization
test_env
.api
.organization_add_project(zeta_organization_id, beta_project_id, USER_USER_PAT)
.await;
// Add friend as a member of the beta project
let resp = test_env
.api
.add_user_to_team(beta_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
.await;
assert_eq!(resp.status(), 204);
// Try to accept invite
// This returns a failure, because since beta and FRIEND are in the organizations,
// they can be added to the project without an invite
let resp = test_env.api.join_team(beta_team_id, FRIEND_USER_PAT).await;
assert_eq!(resp.status(), 400);
// Confirm there is NO owner of the project, as it is owned by the organization
let members = test_env
.api
.get_team_members_deserialized(alpha_team_id, USER_USER_PAT)
.await;
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
assert_eq!(user_member.len(), 0);
// Transfer ownership of zeta organization to FRIEND
let resp = test_env
.api
.transfer_team_ownership(zeta_team_id, FRIEND_USER_ID, USER_USER_PAT)
.await;
assert_eq!(resp.status(), 204);
// Confirm there is NO owner of the project, as it is owned by the organization
let members = test_env
.api
.get_team_members_deserialized(alpha_team_id, USER_USER_PAT)
.await;
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
assert_eq!(user_member.len(), 0);
// Delete organization
let resp = test_env
.api
.delete_organization(zeta_organization_id, FRIEND_USER_PAT)
.await;
assert_eq!(resp.status(), 204);
// Confirm there is only one owner of the alpha project, and it is now FRIEND_USER_ID
let members = test_env
.api
.get_team_members_deserialized(alpha_team_id, USER_USER_PAT)
.await;
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
assert_eq!(user_member.len(), 1);
assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID);
// Confirm there is only one owner of the beta project, and it is now FRIEND_USER_ID
let members = test_env
.api
.get_team_members_deserialized(beta_team_id, USER_USER_PAT)
.await;
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
assert_eq!(user_member.len(), 1);
assert_eq!(user_member[0].user.id.to_string(), FRIEND_USER_ID);
// Confirm there is only one member of the enemy project, and it is STILL ENEMY_USER_ID
let enemy_project = test_env
.api
.get_project_deserialized(&enemy_project.id.to_string(), ENEMY_USER_PAT)
.await;
let members = test_env
.api
.get_team_members_deserialized(&enemy_project.team_id.to_string(), ENEMY_USER_PAT)
.await;
let user_member = members.iter().filter(|m| m.is_owner).collect::<Vec<_>>();
assert_eq!(user_member.len(), 1);
assert_eq!(
user_member[0].user.id.to_string(),
ENEMY_USER_ID_PARSED.to_string()
);
})
.await;
}
#[actix_rt::test]
async fn permissions_patch_organization() {
with_test_environment(None, |test_env: TestEnvironment<ApiV3>| async move {
@@ -480,6 +842,7 @@ async fn permissions_add_remove_project() {
api.organization_remove_project(
&ctx.organization_id.unwrap(),
alpha_project_id,
UserId(FRIEND_USER_ID_PARSED as u64),
ctx.test_pat.as_deref(),
)
.await

View File

@@ -14,6 +14,7 @@ use common::{database::*, scopes::ScopeTest};
use labrinth::models::ids::base62_impl::parse_base62;
use labrinth::models::pats::Scopes;
use labrinth::models::projects::ProjectId;
use labrinth::models::users::UserId;
use serde_json::json;
// For each scope, we (using test_scope):
@@ -1185,8 +1186,13 @@ pub async fn organization_scopes() {
// remove project (now that we've checked)
let req_gen = |pat: Option<String>| async move {
api.organization_remove_project(organization_id, beta_project_id, pat.as_deref())
.await
api.organization_remove_project(
organization_id,
beta_project_id,
UserId(USER_USER_ID_PARSED as u64),
pat.as_deref(),
)
.await
};
ScopeTest::new(&test_env)
.with_failure_scopes(Scopes::all() ^ Scopes::ORGANIZATION_WRITE)

View File

@@ -287,11 +287,12 @@ async fn test_get_team_project_orgs() {
// The team members route from teams (on a project's team):
// - the members of the project team specifically
// - not the ones from the organization
// - Remember: the owner of an org will not be included in the org's team members list
let members = test_env
.api
.get_team_members_deserialized_common(alpha_team_id, FRIEND_USER_PAT)
.await;
assert_eq!(members.len(), 1);
assert_eq!(members.len(), 0);
// The team members route from project should show:
// - the members of the project team including the ones from the organization