More tests (#729)

* permissions tests

* finished permissions; organization tests

* clippy, fmt

* post-merge fixes

* teams changes

* refactored to use new api

* fmt, clippy

* sqlx prepare

* revs

* revs

* re-tested

* re-added name

* reverted to matrix
This commit is contained in:
Wyatt Verchere
2023-10-17 00:53:10 -07:00
committed by GitHub
parent abf4cd71ba
commit 9d0e762f36
27 changed files with 4060 additions and 555 deletions

View File

@@ -19,7 +19,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
rust: [beta, nightly, stable] rust: [stable]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2

View File

@@ -2247,7 +2247,11 @@ pub async fn link_trolley(
} }
if let Some(email) = user.email { 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?; let mut transaction = pool.begin().await?;

View File

@@ -380,7 +380,7 @@ impl User {
redis redis
.delete_many( .delete_many(
user_ids user_ids
.into_iter() .iter()
.map(|id| (USERS_PROJECTS_NAMESPACE, Some(id.0.to_string()))), .map(|id| (USERS_PROJECTS_NAMESPACE, Some(id.0.to_string()))),
) )
.await?; .await?;

View File

@@ -23,7 +23,7 @@ pub struct Team {
} }
bitflags::bitflags! { bitflags::bitflags! {
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct ProjectPermissions: u64 { pub struct ProjectPermissions: u64 {
const UPLOAD_VERSION = 1 << 0; const UPLOAD_VERSION = 1 << 0;
const DELETE_VERSION = 1 << 1; const DELETE_VERSION = 1 << 1;
@@ -35,8 +35,6 @@ bitflags::bitflags! {
const DELETE_PROJECT = 1 << 7; const DELETE_PROJECT = 1 << 7;
const VIEW_ANALYTICS = 1 << 8; const VIEW_ANALYTICS = 1 << 8;
const VIEW_PAYOUTS = 1 << 9; const VIEW_PAYOUTS = 1 << 9;
const ALL = 0b1111111111;
} }
} }
@@ -55,15 +53,19 @@ impl ProjectPermissions {
organization_team_member: &Option<crate::database::models::TeamMember>, // team member of the user in the organization organization_team_member: &Option<crate::database::models::TeamMember>, // team member of the user in the organization
) -> Option<Self> { ) -> Option<Self> {
if role.is_admin() { if role.is_admin() {
return Some(ProjectPermissions::ALL); return Some(ProjectPermissions::all());
} }
if let Some(member) = project_team_member { 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 { 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() { if role.is_mod() {
@@ -79,18 +81,16 @@ impl ProjectPermissions {
} }
bitflags::bitflags! { bitflags::bitflags! {
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct OrganizationPermissions: u64 { pub struct OrganizationPermissions: u64 {
const EDIT_DETAILS = 1 << 0; const EDIT_DETAILS = 1 << 0;
const EDIT_BODY = 1 << 1; const MANAGE_INVITES = 1 << 1;
const MANAGE_INVITES = 1 << 2; const REMOVE_MEMBER = 1 << 2;
const REMOVE_MEMBER = 1 << 3; const EDIT_MEMBER = 1 << 3;
const EDIT_MEMBER = 1 << 4; const ADD_PROJECT = 1 << 4;
const ADD_PROJECT = 1 << 5; const REMOVE_PROJECT = 1 << 5;
const REMOVE_PROJECT = 1 << 6; const DELETE_ORGANIZATION = 1 << 6;
const DELETE_ORGANIZATION = 1 << 8; const EDIT_MEMBER_DEFAULT_PERMISSIONS = 1 << 7; // Separate from EDIT_MEMBER
const EDIT_MEMBER_DEFAULT_PERMISSIONS = 1 << 9; // Separate from EDIT_MEMBER
const ALL = 0b1111111111;
const NONE = 0b0; const NONE = 0b0;
} }
} }
@@ -109,17 +109,17 @@ impl OrganizationPermissions {
team_member: &Option<crate::database::models::TeamMember>, team_member: &Option<crate::database::models::TeamMember>,
) -> Option<Self> { ) -> Option<Self> {
if role.is_admin() { if role.is_admin() {
return Some(OrganizationPermissions::ALL); return Some(OrganizationPermissions::all());
} }
if let Some(member) = team_member { if let Some(member) = team_member {
return member.organization_permissions; if member.accepted {
return member.organization_permissions;
}
} }
if role.is_mod() { if role.is_mod() {
return Some( return Some(
OrganizationPermissions::EDIT_DETAILS OrganizationPermissions::EDIT_DETAILS | OrganizationPermissions::ADD_PROJECT,
| OrganizationPermissions::EDIT_BODY
| OrganizationPermissions::ADD_PROJECT,
); );
} }
None None

View File

@@ -206,7 +206,11 @@ async fn find_version(
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
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>( fn find_file<'a>(

View File

@@ -120,7 +120,7 @@ pub async fn count_download(
analytics_queue.add_download(Download { analytics_queue.add_download(Download {
id: Uuid::new_v4(), 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(), domain: url.host_str().unwrap_or_default().to_string(),
site_path: url.path().to_string(), site_path: url.path().to_string(),
user_id: user user_id: user

View File

@@ -94,8 +94,8 @@ pub async fn organization_create(
members: vec![team_item::TeamMemberBuilder { members: vec![team_item::TeamMemberBuilder {
user_id: current_user.id.into(), user_id: current_user.id.into(),
role: crate::models::teams::OWNER_ROLE.to_owned(), role: crate::models::teams::OWNER_ROLE.to_owned(),
permissions: ProjectPermissions::ALL, permissions: ProjectPermissions::all(),
organization_permissions: Some(OrganizationPermissions::ALL), organization_permissions: Some(OrganizationPermissions::all()),
accepted: true, accepted: true,
payouts_split: Decimal::ONE_HUNDRED, payouts_split: Decimal::ONE_HUNDRED,
ordering: 0, ordering: 0,

View File

@@ -407,12 +407,10 @@ pub async fn add_team_member(
) )
.await? .await?
.1; .1;
let team_association = Team::get_association(team_id, &**pool) let team_association = Team::get_association(team_id, &**pool)
.await? .await?
.ok_or_else(|| ApiError::InvalidInput("The team specified does not exist".to_string()))?; .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?; let member = TeamMember::get_from_user_id(team_id, current_user.id.into(), &**pool).await?;
match team_association { match team_association {
// If team is associated with a project, check if they have permissions to invite users to that project // If team is associated with a project, check if they have permissions to invite users to that project
TeamAssociationId::Project(pid) => { TeamAssociationId::Project(pid) => {
@@ -470,8 +468,8 @@ pub async fn add_team_member(
.contains(OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS) .contains(OrganizationPermissions::EDIT_MEMBER_DEFAULT_PERMISSIONS)
&& !new_member.permissions.is_empty() && !new_member.permissions.is_empty()
{ {
return Err(ApiError::InvalidInput( return Err(ApiError::CustomAuthentication(
"You do not have permission to give this user default project permissions." "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(), .to_string(),
)); ));
} }
@@ -654,8 +652,8 @@ pub async fn edit_team_member(
.unwrap_or_default(); .unwrap_or_default();
if !organization_permissions.contains(OrganizationPermissions::EDIT_MEMBER) { if !organization_permissions.contains(OrganizationPermissions::EDIT_MEMBER) {
return Err(ApiError::InvalidInput( return Err(ApiError::CustomAuthentication(
"You don't have permission to edit organization permissions".to_string(), "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 && !organization_permissions
.contains(OrganizationPermissions::EDIT_MEMBER_DEFAULT_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." "You do not have permission to give this user default project permissions."
.to_string(), .to_string(),
)); ));
@@ -884,7 +882,6 @@ pub async fn remove_team_member(
// removed by a member with the REMOVE_MEMBER permission. // removed by a member with the REMOVE_MEMBER permission.
if Some(delete_member.user_id) == member.as_ref().map(|m| m.user_id) if Some(delete_member.user_id) == member.as_ref().map(|m| m.user_id)
|| permissions.contains(ProjectPermissions::REMOVE_MEMBER) || 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 // 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?; 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) } else if Some(delete_member.user_id) == member.as_ref().map(|m| m.user_id)
|| permissions.contains(ProjectPermissions::MANAGE_INVITES) || 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 // 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 // This is a pending invite rather than a member, so the
@@ -913,49 +909,37 @@ pub async fn remove_team_member(
let organization_permissions = let organization_permissions =
OrganizationPermissions::get_permissions_by_role(&current_user.role, &member) OrganizationPermissions::get_permissions_by_role(&current_user.role, &member)
.unwrap_or_default(); .unwrap_or_default();
if let Some(member) = member { // Organization teams requires a TeamMember, so we can 'unwrap'
// Organization teams requires a TeamMember, so we can 'unwrap' if delete_member.accepted {
if delete_member.accepted { // Members other than the owner can either leave the team, or be
// Members other than the owner can either leave the team, or be // removed by a member with the REMOVE_MEMBER permission.
// removed by a member with the REMOVE_MEMBER permission. if Some(delete_member.user_id) == member.map(|m| m.user_id)
if delete_member.user_id == member.user_id || organization_permissions.contains(OrganizationPermissions::REMOVE_MEMBER)
|| 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
{ {
// 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?; TeamMember::delete(id, user_id, &mut transaction).await?;
} else { } else {
return Err(ApiError::CustomAuthentication( 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(), .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 { } else {
return Err(ApiError::CustomAuthentication( return Err(ApiError::CustomAuthentication(
"You do not have permission to remove a member from this organization" "You do not have permission to cancel an organization invite".to_string(),
.to_string(),
)); ));
} }
} }
} }
TeamMember::clear_cache(id, &redis).await?; 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?; transaction.commit().await?;
Ok(HttpResponse::NoContent().body("")) Ok(HttpResponse::NoContent().body(""))

View File

@@ -18,11 +18,11 @@ pub enum MultipartSegmentData {
} }
pub trait AppendsMultipart { pub trait AppendsMultipart {
fn set_multipart(self, data: Vec<MultipartSegment>) -> Self; fn set_multipart(self, data: impl IntoIterator<Item = MultipartSegment>) -> Self;
} }
impl AppendsMultipart for TestRequest { impl AppendsMultipart for TestRequest {
fn set_multipart(self, data: Vec<MultipartSegment>) -> Self { fn set_multipart(self, data: impl IntoIterator<Item = MultipartSegment>) -> Self {
let (boundary, payload) = generate_multipart(data); let (boundary, payload) = generate_multipart(data);
self.append_header(( self.append_header((
"Content-Type", "Content-Type",
@@ -32,7 +32,7 @@ impl AppendsMultipart for TestRequest {
} }
} }
fn generate_multipart(data: Vec<MultipartSegment>) -> (String, Bytes) { fn generate_multipart(data: impl IntoIterator<Item = MultipartSegment>) -> (String, Bytes) {
let mut boundary = String::from("----WebKitFormBoundary"); let mut boundary = String::from("----WebKitFormBoundary");
boundary.push_str(&rand::random::<u64>().to_string()); boundary.push_str(&rand::random::<u64>().to_string());
boundary.push_str(&rand::random::<u64>().to_string()); boundary.push_str(&rand::random::<u64>().to_string());

View File

@@ -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<dyn LocalService>,
}
impl ApiV2 {
pub async fn call(&self, req: actix_http::Request) -> ServiceResponse {
self.test_app.call(req).await.unwrap()
}
}

View File

@@ -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<Project> {
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<ImageData>,
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
}
}

View File

@@ -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_http::StatusCode;
use actix_web::{ use actix_web::{
dev::ServiceResponse, dev::ServiceResponse,
test::{self, TestRequest}, test::{self, TestRequest},
}; };
use labrinth::models::{ use bytes::Bytes;
notifications::Notification, use labrinth::models::projects::{Project, Version};
projects::{Project, Version},
};
use serde_json::json; use serde_json::json;
use std::rc::Rc;
pub struct ApiV2 { use crate::common::{
pub test_app: Rc<Box<dyn LocalService>>, actix::AppendsMultipart,
} asserts::assert_status,
database::MOD_USER_PAT,
request_data::{ImageData, ProjectCreationRequestData},
};
use super::ApiV2;
impl ApiV2 { impl ApiV2 {
pub async fn call(&self, req: actix_http::Request) -> ServiceResponse {
self.test_app.call(req).await.unwrap()
}
pub async fn add_public_project( pub async fn add_public_project(
&self, &self,
creation_data: ProjectCreationRequestData, creation_data: ProjectCreationRequestData,
) -> (Project, Version) { pat: &str,
) -> (Project, Vec<Version>) {
// Add a project. // Add a project.
let req = TestRequest::post() let req = TestRequest::post()
.uri("/v2/project") .uri("/v2/project")
.append_header(("Authorization", USER_USER_PAT)) .append_header(("Authorization", pat))
.set_multipart(creation_data.segment_data) .set_multipart(creation_data.segment_data)
.to_request(); .to_request();
let resp = self.call(req).await; let resp = self.call(req).await;
@@ -55,19 +45,18 @@ impl ApiV2 {
assert_status(resp, StatusCode::NO_CONTENT); assert_status(resp, StatusCode::NO_CONTENT);
let project = self let project = self
.get_project_deserialized(&creation_data.slug, USER_USER_PAT) .get_project_deserialized(&creation_data.slug, pat)
.await; .await;
// Get project's versions // Get project's versions
let req = TestRequest::get() let req = TestRequest::get()
.uri(&format!("/v2/project/{}/version", creation_data.slug)) .uri(&format!("/v2/project/{}/version", creation_data.slug))
.append_header(("Authorization", USER_USER_PAT)) .append_header(("Authorization", pat))
.to_request(); .to_request();
let resp = self.call(req).await; let resp = self.call(req).await;
let versions: Vec<Version> = test::read_body_json(resp).await; let versions: Vec<Version> = 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 { pub async fn remove_project(&self, project_slug_or_id: &str, pat: &str) -> ServiceResponse {
@@ -80,12 +69,16 @@ impl ApiV2 {
resp 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() let req = TestRequest::get()
.uri(&format!("/v2/project/{slug}")) .uri(&format!("/v2/project/{id_or_slug}"))
.append_header(("Authorization", pat)) .append_header(("Authorization", pat))
.to_request(); .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 test::read_body_json(resp).await
} }
@@ -103,73 +96,94 @@ impl ApiV2 {
test::read_body_json(resp).await test::read_body_json(resp).await
} }
pub async fn add_user_to_team( pub async fn get_version_from_hash(
&self, &self,
team_id: &str, hash: &str,
user_id: &str, algorithm: &str,
pat: &str, pat: &str,
) -> ServiceResponse { ) -> 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"))
.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 get_user_notifications_deserialized(
&self,
user_id: &str,
pat: &str,
) -> Vec<Notification> {
let req = test::TestRequest::get() let req = test::TestRequest::get()
.uri(&format!("/v2/user/{user_id}/notifications")) .uri(&format!("/v2/version_file/{hash}?algorithm={algorithm}"))
.append_header(("Authorization", pat)) .append_header(("Authorization", pat))
.to_request(); .to_request();
let resp = self.call(req).await; self.call(req).await
}
pub async fn get_version_from_hash_deserialized(
&self,
hash: &str,
algorithm: &str,
pat: &str,
) -> Version {
let resp = self.get_version_from_hash(hash, algorithm, pat).await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await test::read_body_json(resp).await
} }
pub async fn mark_notification_read( pub async fn edit_project(
&self, &self,
notification_id: &str, id_or_slug: &str,
patch: serde_json::Value,
pat: &str, pat: &str,
) -> ServiceResponse { ) -> ServiceResponse {
let req = test::TestRequest::patch() let req = test::TestRequest::patch()
.uri(&format!("/v2/notification/{notification_id}")) .uri(&format!("/v2/project/{id_or_slug}"))
.append_header(("Authorization", pat)) .append_header(("Authorization", pat))
.set_json(patch)
.to_request(); .to_request();
self.call(req).await self.call(req).await
} }
pub async fn delete_notification(&self, notification_id: &str, pat: &str) -> ServiceResponse { pub async fn edit_project_bulk(
let req = test::TestRequest::delete() &self,
.uri(&format!("/v2/notification/{notification_id}")) ids_or_slugs: impl IntoIterator<Item = &str>,
patch: serde_json::Value,
pat: &str,
) -> ServiceResponse {
let projects_str = ids_or_slugs
.into_iter()
.map(|s| format!("\"{}\"", s))
.collect::<Vec<_>>()
.join(",");
let req = test::TestRequest::patch()
.uri(&format!(
"/v2/projects?ids={encoded}",
encoded = urlencoding::encode(&format!("[{projects_str}]"))
))
.append_header(("Authorization", pat)) .append_header(("Authorization", pat))
.set_json(patch)
.to_request(); .to_request();
self.call(req).await self.call(req).await
} }
pub async fn edit_project_icon(
&self,
id_or_slug: &str,
icon: Option<ImageData>,
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
}
}
} }

168
tests/common/api_v2/team.rs Normal file
View File

@@ -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<TeamMember> {
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<TeamMember> {
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<TeamMember> {
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<Notification> {
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<ProjectPermissions>,
organization_permissions: Option<OrganizationPermissions>,
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
}
}

View File

@@ -5,6 +5,8 @@ use sqlx::{postgres::PgPoolOptions, PgPool};
use std::time::Duration; use std::time::Duration;
use url::Url; use url::Url;
use crate::common::{dummy_data, environment::TestEnvironment};
// The dummy test database adds a fair bit of 'dummy' data to test with. // 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. // Some constants are used to refer to that data, and are described here.
// The rest can be accessed in the TestEnvironment 'dummy' field. // 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 FRIEND_USER_PAT: &str = "mrp_patfriend";
pub const ENEMY_USER_PAT: &str = "mrp_patenemy"; pub const ENEMY_USER_PAT: &str = "mrp_patenemy";
const TEMPLATE_DATABASE_NAME: &str = "labrinth_tests_template";
#[derive(Clone)] #[derive(Clone)]
pub struct TemporaryDatabase { pub struct TemporaryDatabase {
pub pool: PgPool, pub pool: PgPool,
@@ -37,41 +41,32 @@ pub struct TemporaryDatabase {
} }
impl 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 // 1. Logs into the main database
// 2. Creates a new randomly generated database // 2. Creates a new randomly generated database
// 3. Runs migrations on the new database // 3. Runs migrations on the new database
// 4. (Optionally, by using create_with_dummy) adds dummy data to the 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. // 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. // 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 { pub async fn create(max_connections: Option<u32>) -> Self {
let temp_database_name = generate_random_database_name(); let temp_database_name = generate_random_name("labrinth_tests_db_");
println!("Creating temporary database: {}", &temp_database_name); println!("Creating temporary database: {}", &temp_database_name);
let database_url = dotenvy::var("DATABASE_URL").expect("No database URL"); 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 // Create the temporary (and template datbase, if needed)
let create_db_query = format!("CREATE DATABASE {}", &temp_database_name); Self::create_temporary(&database_url, &temp_database_name).await;
sqlx::query(&create_db_query) // Pool to the temporary database
.execute(&pool) let mut temporary_url = Url::parse(&database_url).expect("Invalid database URL");
.await
.expect("Database creation failed");
pool.close().await; temporary_url.set_path(&format!("/{}", &temp_database_name));
let temp_db_url = temporary_url.to_string();
// Modify the URL to switch to the temporary database
url.set_path(&format!("/{}", &temp_database_name));
let temp_db_url = url.to_string();
let pool = PgPoolOptions::new() let pool = PgPoolOptions::new()
.min_connections(0) .min_connections(0)
.max_connections(4) .max_connections(max_connections.unwrap_or(4))
.max_lifetime(Some(Duration::from_secs(60 * 60))) .max_lifetime(Some(Duration::from_secs(60)))
.connect(&temp_db_url) .connect(&temp_db_url)
.await .await
.expect("Connection to temporary database failed"); .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<i32> = 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. // 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. // 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) { pub async fn cleanup(mut self) {
@@ -125,15 +216,9 @@ impl TemporaryDatabase {
} }
} }
fn generate_random_database_name() -> String { // Appends a random 8-digit number to the end of the str
// Generate a random database name here pub fn generate_random_name(str: &str) -> String {
// You can use your logic to create a unique name let mut str = String::from(str);
// For example, you can use a random string as you did before str.push_str(&rand::random::<u64>().to_string()[..8]);
// or append a timestamp, etc. str
// 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::<u64>().to_string()[..6]);
database_name
} }

View File

@@ -1,5 +1,9 @@
#![allow(dead_code)]
use actix_web::test::{self, TestRequest}; 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 serde_json::json;
use sqlx::Executor; use sqlx::Executor;
@@ -11,8 +15,10 @@ use super::{
request_data::get_public_project_creation_data, request_data::get_public_project_creation_data,
}; };
pub const DUMMY_DATA_UPDATE: i64 = 1;
#[allow(dead_code)] #[allow(dead_code)]
pub const DUMMY_CATEGORIES: &'static [&str] = &[ pub const DUMMY_CATEGORIES: &[&str] = &[
"combat", "combat",
"decoration", "decoration",
"economy", "economy",
@@ -30,65 +36,145 @@ pub enum DummyJarFile {
BasicModDifferent, BasicModDifferent,
} }
#[allow(dead_code)]
pub enum DummyImage {
SmallIcon, // 200x200
}
#[derive(Clone)]
pub struct DummyData { pub struct DummyData {
pub alpha_team_id: String, pub project_alpha: DummyProjectAlpha,
pub beta_team_id: String, pub project_beta: DummyProjectBeta,
pub organization_zeta: DummyOrganizationZeta,
}
pub alpha_project_id: String, #[derive(Clone)]
pub beta_project_id: String, 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_project_slug: String, #[derive(Clone)]
pub beta_project_slug: String, 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_version_id: String, #[derive(Clone)]
pub beta_version_id: String, pub struct DummyOrganizationZeta {
// Zeta organization:
pub alpha_thread_id: String, // This is a dummy organization created by USER user.
pub beta_thread_id: String, // There are no projects in it.
pub organization_id: String,
pub alpha_file_hash: String, pub organization_title: String,
pub beta_file_hash: String, pub team_id: String,
} }
pub async fn add_dummy_data(test_env: &TestEnvironment) -> DummyData { pub async fn add_dummy_data(test_env: &TestEnvironment) -> DummyData {
// Adds basic dummy data to the database directly with sql (user, pats) // Adds basic dummy data to the database directly with sql (user, pats)
let pool = &test_env.db.pool.clone(); let pool = &test_env.db.pool.clone();
pool.execute(include_str!("../files/dummy_data.sql"))
.await pool.execute(
.unwrap(); 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 (alpha_project, alpha_version) = add_project_alpha(test_env).await;
let (beta_project, beta_version) = add_project_beta(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 { DummyData {
alpha_team_id: alpha_project.team.to_string(), project_alpha: DummyProjectAlpha {
beta_team_id: beta_project.team.to_string(), 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(), project_beta: DummyProjectBeta {
beta_project_id: beta_project.id.to_string(), 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_project_slug: alpha_project.slug.unwrap(), organization_zeta: DummyOrganizationZeta {
beta_project_slug: beta_project.slug.unwrap(), organization_id: zeta_organization.id.to_string(),
team_id: zeta_organization.team_id.to_string(),
organization_title: zeta_organization.title,
},
}
}
alpha_version_id: alpha_version.id.to_string(), pub async fn get_dummy_data(test_env: &TestEnvironment) -> DummyData {
beta_version_id: beta_version.id.to_string(), let (alpha_project, alpha_version) = get_project_alpha(test_env).await;
let (beta_project, beta_version) = get_project_beta(test_env).await;
alpha_thread_id: alpha_project.thread_id.to_string(), let zeta_organization = get_organization_zeta(test_env).await;
beta_thread_id: beta_project.thread_id.to_string(), 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_file_hash: alpha_version.files[0].hashes["sha1"].clone(), project_beta: DummyProjectBeta {
beta_file_hash: beta_version.files[0].hashes["sha1"].clone(), 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,
},
} }
} }
pub async fn add_project_alpha(test_env: &TestEnvironment) -> (Project, Version) { pub async fn add_project_alpha(test_env: &TestEnvironment) -> (Project, Version) {
test_env let (project, versions) = test_env
.v2 .v2
.add_public_project(get_public_project_creation_data( .add_public_project(
"alpha", get_public_project_creation_data("alpha", Some(DummyJarFile::DummyProjectAlpha)),
DummyJarFile::DummyProjectAlpha, USER_USER_PAT,
)) )
.await .await;
(project, versions.into_iter().next().unwrap())
} }
pub async fn add_project_beta(test_env: &TestEnvironment) -> (Project, Version) { 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); 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<Version> = 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 // Get project
let req = TestRequest::get() let req = TestRequest::get()
.uri("/v2/project/beta") .uri("/v2/project/beta")
@@ -168,6 +296,18 @@ pub async fn add_project_beta(test_env: &TestEnvironment) -> (Project, Version)
(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 { impl DummyJarFile {
pub fn filename(&self) -> String { pub fn filename(&self) -> String {
match self { 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<u8> {
match self {
DummyImage::SmallIcon => include_bytes!("../../tests/files/200x200.png").to_vec(),
}
}
}

View File

@@ -1,6 +1,6 @@
#![allow(dead_code)] #![allow(dead_code)]
use std::rc::Rc; use std::{rc::Rc, sync::Arc};
use super::{ use super::{
api_v2::ApiV2, api_v2::ApiV2,
@@ -17,7 +17,7 @@ pub async fn with_test_environment<Fut>(f: impl FnOnce(TestEnvironment) -> Fut)
where where
Fut: Future<Output = ()>, Fut: Future<Output = ()>,
{ {
let test_env = TestEnvironment::build_with_dummy().await; let test_env = TestEnvironment::build(None).await;
let db = test_env.db.clone(); let db = test_env.db.clone();
f(test_env).await; f(test_env).await;
@@ -29,27 +29,29 @@ where
// Must be called in an #[actix_rt::test] context. It also simulates a // Must be called in an #[actix_rt::test] context. It also simulates a
// temporary sqlx db like #[sqlx::test] would. // 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. // 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 { pub struct TestEnvironment {
test_app: Rc<Box<dyn LocalService>>, test_app: Rc<dyn LocalService>, // Rc as it's not Send
pub db: TemporaryDatabase, pub db: TemporaryDatabase,
pub v2: ApiV2, pub v2: ApiV2,
pub dummy: Option<dummy_data::DummyData>, pub dummy: Option<Arc<dummy_data::DummyData>>,
} }
impl TestEnvironment { impl TestEnvironment {
pub async fn build_with_dummy() -> Self { pub async fn build(max_connections: Option<u32>) -> Self {
let mut test_env = Self::build().await; let db = TemporaryDatabase::create(max_connections).await;
let dummy = dummy_data::add_dummy_data(&test_env).await; let mut test_env = Self::build_with_db(db).await;
test_env.dummy = Some(dummy);
let dummy = dummy_data::get_dummy_data(&test_env).await;
test_env.dummy = Some(Arc::new(dummy));
test_env test_env
} }
pub async fn build() -> Self { pub async fn build_with_db(db: TemporaryDatabase) -> Self {
let db = TemporaryDatabase::create().await;
let labrinth_config = setup(&db).await; let labrinth_config = setup(&db).await;
let app = App::new().configure(|cfg| labrinth::app_config(cfg, labrinth_config.clone())); let app = App::new().configure(|cfg| labrinth::app_config(cfg, labrinth_config.clone()));
let test_app: Rc<Box<dyn LocalService>> = Rc::new(Box::new(test::init_service(app).await)); let test_app: Rc<dyn LocalService> = Rc::new(test::init_service(app).await);
Self { Self {
v2: ApiV2 { v2: ApiV2 {
test_app: test_app.clone(), test_app: test_app.clone(),
@@ -59,6 +61,7 @@ impl TestEnvironment {
dummy: None, dummy: None,
} }
} }
pub async fn cleanup(self) { pub async fn cleanup(self) {
self.db.cleanup().await; self.db.cleanup().await;
} }
@@ -71,8 +74,10 @@ impl TestEnvironment {
let resp = self let resp = self
.v2 .v2
.add_user_to_team( .add_user_to_team(
&self.dummy.as_ref().unwrap().alpha_team_id, &self.dummy.as_ref().unwrap().project_alpha.team_id,
FRIEND_USER_ID, FRIEND_USER_ID,
None,
None,
USER_USER_PAT, USER_USER_PAT,
) )
.await; .await;

View File

@@ -11,11 +11,12 @@ pub mod database;
pub mod dummy_data; pub mod dummy_data;
pub mod environment; pub mod environment;
pub mod pats; pub mod pats;
pub mod permissions;
pub mod request_data; pub mod request_data;
pub mod scopes; pub mod scopes;
// Testing equivalent to 'setup' function, producing a LabrinthConfig // 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 { pub async fn setup(db: &TemporaryDatabase) -> LabrinthConfig {
println!("Setting up labrinth config"); println!("Setting up labrinth config");

992
tests/common/permissions.rs Normal file
View File

@@ -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<ProjectPermissions>,
failure_organization_permissions: Option<OrganizationPermissions>,
// 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<String>,
project_team_id: Option<String>,
organization_id: Option<String>,
organization_team_id: Option<String>,
// 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<u16>,
}
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<ProjectPermissions>,
failure_organization_permissions: Option<OrganizationPermissions>,
) -> 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<Item = u16>,
) -> 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<T>(
&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<T>(
&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<T>(
&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<T>(
&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<ProjectPermissions>,
organization_permissions: Option<OrganizationPermissions>,
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<ProjectPermissions>,
organization_permissions: Option<OrganizationPermissions>,
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)
}

View File

@@ -1,18 +1,45 @@
#![allow(dead_code)]
use serde_json::json; use serde_json::json;
use super::{actix::MultipartSegment, dummy_data::DummyJarFile}; use super::{
actix::MultipartSegment,
dummy_data::{DummyImage, DummyJarFile},
};
use crate::common::actix::MultipartSegmentData; use crate::common::actix::MultipartSegmentData;
pub struct ProjectCreationRequestData { pub struct ProjectCreationRequestData {
pub slug: String, pub slug: String,
pub jar: DummyJarFile, pub jar: Option<DummyJarFile>,
pub segment_data: Vec<MultipartSegment>, pub segment_data: Vec<MultipartSegment>,
} }
pub struct ImageData {
pub filename: String,
pub extension: String,
pub icon: Vec<u8>,
}
pub fn get_public_project_creation_data( pub fn get_public_project_creation_data(
slug: &str, slug: &str,
jar: DummyJarFile, version_jar: Option<DummyJarFile>,
) -> ProjectCreationRequestData { ) -> 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!( let json_data = json!(
{ {
"title": format!("Test Project {slug}"), "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.", "body": "This project is approved, and versions are listed.",
"client_side": "required", "client_side": "required",
"server_side": "optional", "server_side": "optional",
"initial_versions": [{ "initial_versions": initial_versions,
"file_parts": [jar.filename()], "is_draft": is_draft,
"version_number": "1.2.3",
"version_title": "start",
"dependencies": [],
"game_versions": ["1.20.1"] ,
"release_channel": "release",
"loaders": ["fabric"],
"featured": true
}],
"categories": [], "categories": [],
"license_id": "MIT" "license_id": "MIT"
} }
@@ -44,17 +63,31 @@ pub fn get_public_project_creation_data(
data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()), data: MultipartSegmentData::Text(serde_json::to_string(&json_data).unwrap()),
}; };
// Basic file let segment_data = if let Some(ref jar) = version_jar {
let file_segment = MultipartSegment { // Basic file
name: jar.filename(), let file_segment = MultipartSegment {
filename: Some(jar.filename()), name: jar.filename(),
content_type: Some("application/java-archive".to_string()), filename: Some(jar.filename()),
data: MultipartSegmentData::Binary(jar.bytes()), 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 { ProjectCreationRequestData {
slug: slug.to_string(), slug: slug.to_string(),
jar, jar: version_jar,
segment_data: vec![json_segment.clone(), file_segment.clone()], segment_data,
}
}
pub fn get_icon_data(dummy_icon: DummyImage) -> ImageData {
ImageData {
filename: dummy_icon.filename(),
extension: dummy_icon.extension(),
icon: dummy_icon.bytes(),
} }
} }

View File

@@ -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 -- 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 -- 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 -- 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 (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', 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', $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', 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', $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', 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', $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', 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', $1, '2030-08-18 15:48:58.435729+00');
-- -- Sample game versions, loaders, categories -- -- Sample game versions, loaders, categories
INSERT INTO game_versions (id, version, type, created) INSERT INTO game_versions (id, version, type, created)
@@ -44,3 +44,8 @@ INSERT INTO categories (id, category, project_type) VALUES
(105, 'magic', 2), (105, 'magic', 2),
(106, 'mobs', 2), (106, 'mobs', 2),
(107, 'optimization', 2); (107, 'optimization', 2);
-- Create dummy data table to mark that this file has been run
CREATE TABLE dummy_data (
update_id bigint PRIMARY KEY
);

View File

@@ -8,12 +8,18 @@ mod common;
#[actix_rt::test] #[actix_rt::test]
pub async fn get_user_notifications_after_team_invitation_returns_notification() { pub async fn get_user_notifications_after_team_invitation_returns_notification() {
with_test_environment(|test_env| async move { 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; let api = test_env.v2;
api.get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) api.get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT)
.await; .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; .await;
let notifications = api let notifications = api

649
tests/organizations.rs Normal file
View File

@@ -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;
}

View File

@@ -18,7 +18,7 @@ mod common;
// - ensure PATs can be deleted // - ensure PATs can be deleted
#[actix_rt::test] #[actix_rt::test]
pub async fn pat_full_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 // Create a PAT for a full test
let req = test::TestRequest::post() let req = test::TestRequest::post()
@@ -163,7 +163,7 @@ pub async fn pat_full_test() {
// Test illegal PAT setting, both in POST and PATCH // Test illegal PAT setting, both in POST and PATCH
#[actix_rt::test] #[actix_rt::test]
pub async fn bad_pats() { 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 // Creating a PAT with no name should fail
let req = test::TestRequest::post() let req = test::TestRequest::post()

File diff suppressed because it is too large Load Diff

View File

@@ -20,7 +20,7 @@ mod common;
#[actix_rt::test] #[actix_rt::test]
async fn user_scopes() { async fn user_scopes() {
// Test setup and dummy data // Test setup and dummy data
let test_env = TestEnvironment::build_with_dummy().await; let test_env = TestEnvironment::build(None).await;
// User reading // User reading
let read_user = Scopes::USER_READ; let read_user = Scopes::USER_READ;
@@ -87,14 +87,20 @@ async fn user_scopes() {
// Notifications // Notifications
#[actix_rt::test] #[actix_rt::test]
pub async fn notifications_scopes() { pub async fn notifications_scopes() {
let test_env = TestEnvironment::build_with_dummy().await; let test_env = TestEnvironment::build(None).await;
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();
// We will invite user 'friend' to project team, and use that as a notification // We will invite user 'friend' to project team, and use that as a notification
// Get notifications // Get notifications
let resp = test_env let resp = test_env
.v2 .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; .await;
assert_eq!(resp.status(), 204); assert_eq!(resp.status(), 204);
@@ -107,7 +113,7 @@ pub async fn notifications_scopes() {
.test(req_gen, read_notifications) .test(req_gen, read_notifications)
.await .await
.unwrap(); .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 = || { let req_gen = || {
test::TestRequest::get().uri(&format!( 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 // We invite mod, get the notification ID, and do mass delete using that
let resp = test_env let resp = test_env
.v2 .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; .await;
assert_eq!(resp.status(), 204); assert_eq!(resp.status(), 204);
let read_notifications = Scopes::NOTIFICATION_READ; let read_notifications = Scopes::NOTIFICATION_READ;
@@ -172,7 +178,7 @@ pub async fn notifications_scopes() {
.test(req_gen, read_notifications) .test(req_gen, read_notifications)
.await .await
.unwrap(); .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 = || { let req_gen = || {
test::TestRequest::delete().uri(&format!( test::TestRequest::delete().uri(&format!(
@@ -193,7 +199,7 @@ pub async fn notifications_scopes() {
// Project version creation scopes // Project version creation scopes
#[actix_rt::test] #[actix_rt::test]
pub async fn project_version_create_scopes() { pub async fn project_version_create_scopes() {
let test_env = TestEnvironment::build_with_dummy().await; let test_env = TestEnvironment::build(None).await;
// Create project // Create project
let create_project = Scopes::PROJECT_CREATE; let create_project = Scopes::PROJECT_CREATE;
@@ -292,11 +298,35 @@ pub async fn project_version_create_scopes() {
// Project management scopes // Project management scopes
#[actix_rt::test] #[actix_rt::test]
pub async fn project_version_reads_scopes() { pub async fn project_version_reads_scopes() {
let test_env = TestEnvironment::build_with_dummy().await; let test_env = TestEnvironment::build(None).await;
let beta_project_id = &test_env.dummy.as_ref().unwrap().beta_project_id.clone(); let beta_project_id = &test_env
let beta_version_id = &test_env.dummy.as_ref().unwrap().beta_version_id.clone(); .dummy
let alpha_team_id = &test_env.dummy.as_ref().unwrap().alpha_team_id.clone(); .as_ref()
let beta_file_hash = &test_env.dummy.as_ref().unwrap().beta_file_hash.clone(); .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 // Project reading
// Uses 404 as the expected failure code (or 200 and an empty list for mass reads) // 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) .test(req_gen, read_project)
.await .await
.unwrap(); .unwrap();
assert!(!failure.as_array().unwrap()[0].as_object().unwrap()["permissions"].is_number()); assert!(!failure[0]["permissions"].is_number());
assert!(success.as_array().unwrap()[0].as_object().unwrap()["permissions"].is_number()); assert!(success[0]["permissions"].is_number());
let req_gen = || { let req_gen = || {
test::TestRequest::get().uri(&format!( test::TestRequest::get().uri(&format!(
@@ -362,14 +392,8 @@ pub async fn project_version_reads_scopes() {
.test(req_gen, read_project) .test(req_gen, read_project)
.await .await
.unwrap(); .unwrap();
assert!(!failure.as_array().unwrap()[0].as_array().unwrap()[0] assert!(!failure[0][0]["permissions"].is_number());
.as_object() assert!(success[0][0]["permissions"].is_number());
.unwrap()["permissions"]
.is_number());
assert!(success.as_array().unwrap()[0].as_array().unwrap()[0]
.as_object()
.unwrap()["permissions"]
.is_number());
// User project reading // User project reading
// Test user has two projects, one public and one private // Test user has two projects, one public and one private
@@ -510,9 +534,21 @@ pub async fn project_version_reads_scopes() {
#[actix_rt::test] #[actix_rt::test]
pub async fn project_write_scopes() { pub async fn project_write_scopes() {
// Test setup and dummy data // Test setup and dummy data
let test_env = TestEnvironment::build_with_dummy().await; let test_env = TestEnvironment::build(None).await;
let beta_project_id = &test_env.dummy.as_ref().unwrap().beta_project_id.clone(); let beta_project_id = &test_env
let alpha_team_id = &test_env.dummy.as_ref().unwrap().alpha_team_id.clone(); .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 // Projects writing
let write_project = Scopes::PROJECT_WRITE; let write_project = Scopes::PROJECT_WRITE;
@@ -714,10 +750,28 @@ pub async fn project_write_scopes() {
#[actix_rt::test] #[actix_rt::test]
pub async fn version_write_scopes() { pub async fn version_write_scopes() {
// Test setup and dummy data // Test setup and dummy data
let test_env = TestEnvironment::build_with_dummy().await; let test_env = TestEnvironment::build(None).await;
let alpha_version_id = &test_env.dummy.as_ref().unwrap().beta_version_id.clone(); let alpha_version_id = &test_env
let beta_version_id = &test_env.dummy.as_ref().unwrap().beta_version_id.clone(); .dummy
let alpha_file_hash = &test_env.dummy.as_ref().unwrap().beta_file_hash.clone(); .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; let write_version = Scopes::VERSION_WRITE;
@@ -829,8 +883,14 @@ pub async fn version_write_scopes() {
#[actix_rt::test] #[actix_rt::test]
pub async fn report_scopes() { pub async fn report_scopes() {
// Test setup and dummy data // Test setup and dummy data
let test_env = TestEnvironment::build_with_dummy().await; let test_env = TestEnvironment::build(None).await;
let beta_project_id = &test_env.dummy.as_ref().unwrap().beta_project_id.clone(); let beta_project_id = &test_env
.dummy
.as_ref()
.unwrap()
.project_beta
.project_id
.clone();
// Create report // Create report
let report_create = Scopes::REPORT_CREATE; let report_create = Scopes::REPORT_CREATE;
@@ -854,7 +914,7 @@ pub async fn report_scopes() {
.test(req_gen, report_read) .test(req_gen, report_read)
.await .await
.unwrap(); .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)); let req_gen = || test::TestRequest::get().uri(&format!("/v2/report/{}", report_id));
ScopeTest::new(&test_env) ScopeTest::new(&test_env)
@@ -905,9 +965,21 @@ pub async fn report_scopes() {
#[actix_rt::test] #[actix_rt::test]
pub async fn thread_scopes() { pub async fn thread_scopes() {
// Test setup and dummy data // Test setup and dummy data
let test_env = TestEnvironment::build_with_dummy().await; let test_env = TestEnvironment::build(None).await;
let alpha_thread_id = &test_env.dummy.as_ref().unwrap().alpha_thread_id.clone(); let alpha_thread_id = &test_env
let beta_thread_id = &test_env.dummy.as_ref().unwrap().beta_thread_id.clone(); .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 // Thread read
let thread_read = Scopes::THREAD_READ; let thread_read = Scopes::THREAD_READ;
@@ -954,8 +1026,7 @@ pub async fn thread_scopes() {
.test(req_gen, thread_read) .test(req_gen, thread_read)
.await .await
.unwrap(); .unwrap();
let thread = success.as_array().unwrap()[0].as_object().unwrap(); let thread_id = success[0]["id"].as_str().unwrap();
let thread_id = thread["id"].as_str().unwrap();
// Moderator 'read' thread // Moderator 'read' thread
// Uses moderator PAT, as only moderators can see the moderation inbox // Uses moderator PAT, as only moderators can see the moderation inbox
@@ -974,10 +1045,8 @@ pub async fn thread_scopes() {
.to_request(); .to_request();
let resp = test_env.call(req_gen).await; let resp = test_env.call(req_gen).await;
let success: serde_json::Value = test::read_body_json(resp).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 = success["messages"][0]["id"].as_str().unwrap();
let thread_message_id = thread_messages[0].as_object().unwrap()["id"]
.as_str()
.unwrap();
let req_gen = || test::TestRequest::delete().uri(&format!("/v2/message/{thread_message_id}")); let req_gen = || test::TestRequest::delete().uri(&format!("/v2/message/{thread_message_id}"));
ScopeTest::new(&test_env) ScopeTest::new(&test_env)
.with_user_id(MOD_USER_ID_PARSED) .with_user_id(MOD_USER_ID_PARSED)
@@ -992,7 +1061,7 @@ pub async fn thread_scopes() {
// Pat scopes // Pat scopes
#[actix_rt::test] #[actix_rt::test]
pub async fn pat_scopes() { pub async fn pat_scopes() {
let test_env = TestEnvironment::build_with_dummy().await; let test_env = TestEnvironment::build(None).await;
// Pat create // Pat create
let pat_create = Scopes::PAT_CREATE; let pat_create = Scopes::PAT_CREATE;
@@ -1045,8 +1114,14 @@ pub async fn pat_scopes() {
#[actix_rt::test] #[actix_rt::test]
pub async fn collections_scopes() { pub async fn collections_scopes() {
// Test setup and dummy data // Test setup and dummy data
let test_env = TestEnvironment::build_with_dummy().await; let test_env = TestEnvironment::build(None).await;
let alpha_project_id = &test_env.dummy.as_ref().unwrap().alpha_project_id.clone(); let alpha_project_id = &test_env
.dummy
.as_ref()
.unwrap()
.project_alpha
.project_id
.clone();
// Create collection // Create collection
let collection_create = Scopes::COLLECTION_CREATE; let collection_create = Scopes::COLLECTION_CREATE;
@@ -1140,8 +1215,14 @@ pub async fn collections_scopes() {
#[actix_rt::test] #[actix_rt::test]
pub async fn organization_scopes() { pub async fn organization_scopes() {
// Test setup and dummy data // Test setup and dummy data
let test_env = TestEnvironment::build_with_dummy().await; let test_env = TestEnvironment::build(None).await;
let beta_project_id = &test_env.dummy.as_ref().unwrap().beta_project_id.clone(); let beta_project_id = &test_env
.dummy
.as_ref()
.unwrap()
.project_beta
.project_id
.clone();
// Create organization // Create organization
let organization_create = Scopes::ORGANIZATION_CREATE; let organization_create = Scopes::ORGANIZATION_CREATE;
@@ -1215,18 +1296,8 @@ pub async fn organization_scopes() {
.test(req_gen, organization_read) .test(req_gen, organization_read)
.await .await
.unwrap(); .unwrap();
assert!( assert!(failure["members"][0]["permissions"].is_null());
failure.as_object().unwrap()["members"].as_array().unwrap()[0] assert!(!success["members"][0]["permissions"].is_null());
.as_object()
.unwrap()["permissions"]
.is_null()
);
assert!(
!success.as_object().unwrap()["members"].as_array().unwrap()[0]
.as_object()
.unwrap()["permissions"]
.is_null()
);
let req_gen = || { let req_gen = || {
test::TestRequest::get().uri(&format!( test::TestRequest::get().uri(&format!(
@@ -1240,22 +1311,8 @@ pub async fn organization_scopes() {
.test(req_gen, organization_read) .test(req_gen, organization_read)
.await .await
.unwrap(); .unwrap();
assert!( assert!(failure[0]["members"][0]["permissions"].is_null());
failure.as_array().unwrap()[0].as_object().unwrap()["members"] assert!(!success[0]["members"][0]["permissions"].is_null());
.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()
);
let organization_project_read = Scopes::PROJECT_READ | Scopes::ORGANIZATION_READ; let organization_project_read = Scopes::PROJECT_READ | Scopes::ORGANIZATION_READ;
let req_gen = let req_gen =
@@ -1300,6 +1357,4 @@ pub async fn organization_scopes() {
// TODO: Some hash/version files functions // TODO: Some hash/version files functions
// TODO: Meta pat stuff
// TODO: Image scopes // TODO: Image scopes

661
tests/teams.rs Normal file
View File

@@ -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;
// }

View File

@@ -7,6 +7,15 @@ use crate::common::{dummy_data::DummyJarFile, request_data::get_public_project_c
mod common; 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] #[actix_rt::test]
pub async fn get_user_projects_after_creating_project_returns_new_project() { pub async fn get_user_projects_after_creating_project_returns_new_project() {
with_test_environment(|test_env| async move { with_test_environment(|test_env| async move {
@@ -15,10 +24,10 @@ pub async fn get_user_projects_after_creating_project_returns_new_project() {
.await; .await;
let (project, _) = api let (project, _) = api
.add_public_project(get_public_project_creation_data( .add_public_project(
"slug", get_public_project_creation_data("slug", Some(DummyJarFile::BasicMod)),
DummyJarFile::BasicMod, USER_USER_PAT,
)) )
.await; .await;
let resp_projects = api 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 { with_test_environment(|test_env| async move {
let api = test_env.v2; let api = test_env.v2;
let (project, _) = api let (project, _) = api
.add_public_project(get_public_project_creation_data( .add_public_project(
"iota", get_public_project_creation_data("iota", Some(DummyJarFile::BasicMod)),
DummyJarFile::BasicMod, USER_USER_PAT,
)) )
.await; .await;
api.get_user_projects_deserialized(USER_USER_ID, USER_USER_PAT) api.get_user_projects_deserialized(USER_USER_ID, USER_USER_PAT)
.await; .await;
api.remove_project(&project.slug.as_ref().unwrap(), USER_USER_PAT) api.remove_project(project.slug.as_ref().unwrap(), USER_USER_PAT)
.await; .await;
let resp_projects = api let resp_projects = api
@@ -56,15 +65,15 @@ pub async fn get_user_projects_after_deleting_project_shows_removal() {
#[actix_rt::test] #[actix_rt::test]
pub async fn get_user_projects_after_joining_team_shows_team_projects() { pub async fn get_user_projects_after_joining_team_shows_team_projects() {
with_test_environment(|test_env| async move { with_test_environment(|test_env| async move {
let alpha_team_id = &test_env.dummy.as_ref().unwrap().alpha_team_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().alpha_project_id; let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id;
let api = test_env.v2; let api = test_env.v2;
api.get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) api.get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT)
.await; .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; .await;
api.join_team(&alpha_team_id, FRIEND_USER_PAT).await; api.join_team(alpha_team_id, FRIEND_USER_PAT).await;
let projects = api let projects = api
.get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT) .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] #[actix_rt::test]
pub async fn get_user_projects_after_leaving_team_shows_no_team_projects() { pub async fn get_user_projects_after_leaving_team_shows_no_team_projects() {
with_test_environment(|test_env| async move { with_test_environment(|test_env| async move {
let alpha_team_id = &test_env.dummy.as_ref().unwrap().alpha_team_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().alpha_project_id; let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id;
let api = test_env.v2; 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; .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) api.get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT)
.await; .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; .await;
let projects = api let projects = api