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:
matrix:
os: [ubuntu-latest]
rust: [beta, nightly, stable]
rust: [stable]
steps:
- uses: actions/checkout@v2

View File

@@ -2247,7 +2247,11 @@ pub async fn link_trolley(
}
if let Some(email) = user.email {
let id = payouts_queue.lock().await.register_recipient(&email, body.0).await?;
let id = payouts_queue
.lock()
.await
.register_recipient(&email, body.0)
.await?;
let mut transaction = pool.begin().await?;

View File

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

View File

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

View File

@@ -206,7 +206,11 @@ async fn find_version(
})
.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>(

View File

@@ -120,7 +120,7 @@ pub async fn count_download(
analytics_queue.add_download(Download {
id: Uuid::new_v4(),
recorded: get_current_tenths_of_ms(),
recorded: get_current_tenths_of_ms(),
domain: url.host_str().unwrap_or_default().to_string(),
site_path: url.path().to_string(),
user_id: user

View File

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

View File

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

View File

@@ -18,11 +18,11 @@ pub enum MultipartSegmentData {
}
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 {
fn set_multipart(self, data: Vec<MultipartSegment>) -> Self {
fn set_multipart(self, data: impl IntoIterator<Item = MultipartSegment>) -> Self {
let (boundary, payload) = generate_multipart(data);
self.append_header((
"Content-Type",
@@ -32,7 +32,7 @@ impl AppendsMultipart for TestRequest {
}
}
fn generate_multipart(data: Vec<MultipartSegment>) -> (String, Bytes) {
fn generate_multipart(data: impl IntoIterator<Item = MultipartSegment>) -> (String, Bytes) {
let mut boundary = String::from("----WebKitFormBoundary");
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_web::{
dev::ServiceResponse,
test::{self, TestRequest},
};
use labrinth::models::{
notifications::Notification,
projects::{Project, Version},
};
use bytes::Bytes;
use labrinth::models::projects::{Project, Version};
use serde_json::json;
use std::rc::Rc;
pub struct ApiV2 {
pub test_app: Rc<Box<dyn LocalService>>,
}
use crate::common::{
actix::AppendsMultipart,
asserts::assert_status,
database::MOD_USER_PAT,
request_data::{ImageData, ProjectCreationRequestData},
};
use super::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(
&self,
creation_data: ProjectCreationRequestData,
) -> (Project, Version) {
pat: &str,
) -> (Project, Vec<Version>) {
// Add a project.
let req = TestRequest::post()
.uri("/v2/project")
.append_header(("Authorization", USER_USER_PAT))
.append_header(("Authorization", pat))
.set_multipart(creation_data.segment_data)
.to_request();
let resp = self.call(req).await;
@@ -55,19 +45,18 @@ impl ApiV2 {
assert_status(resp, StatusCode::NO_CONTENT);
let project = self
.get_project_deserialized(&creation_data.slug, USER_USER_PAT)
.get_project_deserialized(&creation_data.slug, pat)
.await;
// Get project's versions
let req = TestRequest::get()
.uri(&format!("/v2/project/{}/version", creation_data.slug))
.append_header(("Authorization", USER_USER_PAT))
.append_header(("Authorization", pat))
.to_request();
let resp = self.call(req).await;
let versions: Vec<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 {
@@ -80,12 +69,16 @@ impl ApiV2 {
resp
}
pub async fn get_project_deserialized(&self, slug: &str, pat: &str) -> Project {
pub async fn get_project(&self, id_or_slug: &str, pat: &str) -> ServiceResponse {
let req = TestRequest::get()
.uri(&format!("/v2/project/{slug}"))
.uri(&format!("/v2/project/{id_or_slug}"))
.append_header(("Authorization", pat))
.to_request();
let resp = self.call(req).await;
self.call(req).await
}
pub async fn get_project_deserialized(&self, id_or_slug: &str, pat: &str) -> Project {
let resp = self.get_project(id_or_slug, pat).await;
assert_eq!(resp.status(), 200);
test::read_body_json(resp).await
}
@@ -103,73 +96,94 @@ impl ApiV2 {
test::read_body_json(resp).await
}
pub async fn add_user_to_team(
pub async fn get_version_from_hash(
&self,
team_id: &str,
user_id: &str,
hash: &str,
algorithm: &str,
pat: &str,
) -> ServiceResponse {
let req = test::TestRequest::post()
.uri(&format!("/v2/team/{team_id}/members"))
.append_header(("Authorization", pat))
.set_json(json!( {
"user_id": user_id
}))
.to_request();
self.call(req).await
}
pub async fn join_team(&self, team_id: &str, pat: &str) -> ServiceResponse {
let req = test::TestRequest::post()
.uri(&format!("/v2/team/{team_id}/join"))
.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()
.uri(&format!("/v2/user/{user_id}/notifications"))
.uri(&format!("/v2/version_file/{hash}?algorithm={algorithm}"))
.append_header(("Authorization", pat))
.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
}
pub async fn mark_notification_read(
pub async fn edit_project(
&self,
notification_id: &str,
id_or_slug: &str,
patch: serde_json::Value,
pat: &str,
) -> ServiceResponse {
let req = test::TestRequest::patch()
.uri(&format!("/v2/notification/{notification_id}"))
.uri(&format!("/v2/project/{id_or_slug}"))
.append_header(("Authorization", pat))
.set_json(patch)
.to_request();
self.call(req).await
}
pub async fn delete_notification(&self, notification_id: &str, pat: &str) -> ServiceResponse {
let req = test::TestRequest::delete()
.uri(&format!("/v2/notification/{notification_id}"))
pub async fn edit_project_bulk(
&self,
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))
.set_json(patch)
.to_request();
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 url::Url;
use crate::common::{dummy_data, environment::TestEnvironment};
// The dummy test database adds a fair bit of 'dummy' data to test with.
// Some constants are used to refer to that data, and are described here.
// The rest can be accessed in the TestEnvironment 'dummy' field.
@@ -29,6 +31,8 @@ pub const USER_USER_PAT: &str = "mrp_patuser";
pub const FRIEND_USER_PAT: &str = "mrp_patfriend";
pub const ENEMY_USER_PAT: &str = "mrp_patenemy";
const TEMPLATE_DATABASE_NAME: &str = "labrinth_tests_template";
#[derive(Clone)]
pub struct TemporaryDatabase {
pub pool: PgPool,
@@ -37,41 +41,32 @@ pub struct TemporaryDatabase {
}
impl TemporaryDatabase {
// Creates a temporary database like sqlx::test does
// Creates a temporary database like sqlx::test does (panics)
// 1. Logs into the main database
// 2. Creates a new randomly generated database
// 3. Runs migrations on the new database
// 4. (Optionally, by using create_with_dummy) adds dummy data to the database
// If a db is created with create_with_dummy, it must be cleaned up with cleanup.
// This means that dbs will only 'remain' if a test fails (for examination of the db), and will be cleaned up otherwise.
pub async fn create() -> Self {
let temp_database_name = generate_random_database_name();
pub async fn create(max_connections: Option<u32>) -> Self {
let temp_database_name = generate_random_name("labrinth_tests_db_");
println!("Creating temporary database: {}", &temp_database_name);
let database_url = dotenvy::var("DATABASE_URL").expect("No database URL");
let mut url = Url::parse(&database_url).expect("Invalid database URL");
let pool = PgPool::connect(&database_url)
.await
.expect("Connection to database failed");
// Create the temporary database
let create_db_query = format!("CREATE DATABASE {}", &temp_database_name);
// Create the temporary (and template datbase, if needed)
Self::create_temporary(&database_url, &temp_database_name).await;
sqlx::query(&create_db_query)
.execute(&pool)
.await
.expect("Database creation failed");
// Pool to the temporary database
let mut temporary_url = Url::parse(&database_url).expect("Invalid database URL");
pool.close().await;
// Modify the URL to switch to the temporary database
url.set_path(&format!("/{}", &temp_database_name));
let temp_db_url = url.to_string();
temporary_url.set_path(&format!("/{}", &temp_database_name));
let temp_db_url = temporary_url.to_string();
let pool = PgPoolOptions::new()
.min_connections(0)
.max_connections(4)
.max_lifetime(Some(Duration::from_secs(60 * 60)))
.max_connections(max_connections.unwrap_or(4))
.max_lifetime(Some(Duration::from_secs(60)))
.connect(&temp_db_url)
.await
.expect("Connection to temporary database failed");
@@ -94,7 +89,103 @@ impl TemporaryDatabase {
}
}
// Deletes the temporary database
// Creates a template and temporary databse (panics)
// 1. Waits to obtain a pg lock on the main database
// 2. Creates a new template database called 'TEMPLATE_DATABASE_NAME', if needed
// 3. Switches to the template database
// 4. Runs migrations on the new database (for most tests, this should not take time)
// 5. Creates dummy data on the new db
// 6. Creates a temporary database at 'temp_database_name' from the template
// 7. Drops lock and all created connections in the function
async fn create_temporary(database_url: &str, temp_database_name: &str) {
let main_pool = PgPool::connect(database_url)
.await
.expect("Connection to database failed");
loop {
// Try to acquire an advisory lock
let lock_acquired: bool = sqlx::query_scalar("SELECT pg_try_advisory_lock(1)")
.fetch_one(&main_pool)
.await
.unwrap();
if lock_acquired {
// Create the db template if it doesn't exist
// Check if template_db already exists
let db_exists: Option<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.
// This means that dbs will only 'remain' if a test fails (for examination of the db), and will be cleaned up otherwise.
pub async fn cleanup(mut self) {
@@ -125,15 +216,9 @@ impl TemporaryDatabase {
}
}
fn generate_random_database_name() -> String {
// Generate a random database name here
// You can use your logic to create a unique name
// For example, you can use a random string as you did before
// or append a timestamp, etc.
// We will use a random string starting with "labrinth_tests_db_"
// and append a 6-digit number to it.
let mut database_name = String::from("labrinth_tests_db_");
database_name.push_str(&rand::random::<u64>().to_string()[..6]);
database_name
// Appends a random 8-digit number to the end of the str
pub fn generate_random_name(str: &str) -> String {
let mut str = String::from(str);
str.push_str(&rand::random::<u64>().to_string()[..8]);
str
}

View File

@@ -1,5 +1,9 @@
#![allow(dead_code)]
use actix_web::test::{self, TestRequest};
use labrinth::{models::projects::Project, models::projects::Version};
use labrinth::{
models::projects::Project,
models::{organizations::Organization, pats::Scopes, projects::Version},
};
use serde_json::json;
use sqlx::Executor;
@@ -11,8 +15,10 @@ use super::{
request_data::get_public_project_creation_data,
};
pub const DUMMY_DATA_UPDATE: i64 = 1;
#[allow(dead_code)]
pub const DUMMY_CATEGORIES: &'static [&str] = &[
pub const DUMMY_CATEGORIES: &[&str] = &[
"combat",
"decoration",
"economy",
@@ -30,65 +36,145 @@ pub enum DummyJarFile {
BasicModDifferent,
}
#[allow(dead_code)]
pub enum DummyImage {
SmallIcon, // 200x200
}
#[derive(Clone)]
pub struct DummyData {
pub alpha_team_id: String,
pub beta_team_id: String,
pub project_alpha: DummyProjectAlpha,
pub project_beta: DummyProjectBeta,
pub organization_zeta: DummyOrganizationZeta,
}
pub alpha_project_id: String,
pub beta_project_id: String,
#[derive(Clone)]
pub struct DummyProjectAlpha {
// Alpha project:
// This is a dummy project created by USER user.
// It's approved, listed, and visible to the public.
pub project_id: String,
pub project_slug: String,
pub version_id: String,
pub thread_id: String,
pub file_hash: String,
pub team_id: String,
}
pub alpha_project_slug: String,
pub beta_project_slug: String,
#[derive(Clone)]
pub struct DummyProjectBeta {
// Beta project:
// This is a dummy project created by USER user.
// It's not approved, unlisted, and not visible to the public.
pub project_id: String,
pub project_slug: String,
pub version_id: String,
pub thread_id: String,
pub file_hash: String,
pub team_id: String,
}
pub alpha_version_id: String,
pub beta_version_id: String,
pub alpha_thread_id: String,
pub beta_thread_id: String,
pub alpha_file_hash: String,
pub beta_file_hash: String,
#[derive(Clone)]
pub struct DummyOrganizationZeta {
// Zeta organization:
// This is a dummy organization created by USER user.
// There are no projects in it.
pub organization_id: String,
pub organization_title: String,
pub team_id: String,
}
pub async fn add_dummy_data(test_env: &TestEnvironment) -> DummyData {
// Adds basic dummy data to the database directly with sql (user, pats)
let pool = &test_env.db.pool.clone();
pool.execute(include_str!("../files/dummy_data.sql"))
.await
.unwrap();
pool.execute(
include_str!("../files/dummy_data.sql")
.replace("$1", &Scopes::all().bits().to_string())
.as_str(),
)
.await
.unwrap();
let (alpha_project, alpha_version) = add_project_alpha(test_env).await;
let (beta_project, beta_version) = add_project_beta(test_env).await;
let zeta_organization = add_organization_zeta(test_env).await;
sqlx::query("INSERT INTO dummy_data (update_id) VALUES ($1)")
.bind(DUMMY_DATA_UPDATE)
.execute(pool)
.await
.unwrap();
DummyData {
alpha_team_id: alpha_project.team.to_string(),
beta_team_id: beta_project.team.to_string(),
project_alpha: DummyProjectAlpha {
team_id: alpha_project.team.to_string(),
project_id: alpha_project.id.to_string(),
project_slug: alpha_project.slug.unwrap(),
version_id: alpha_version.id.to_string(),
thread_id: alpha_project.thread_id.to_string(),
file_hash: alpha_version.files[0].hashes["sha1"].clone(),
},
alpha_project_id: alpha_project.id.to_string(),
beta_project_id: beta_project.id.to_string(),
project_beta: DummyProjectBeta {
team_id: beta_project.team.to_string(),
project_id: beta_project.id.to_string(),
project_slug: beta_project.slug.unwrap(),
version_id: beta_version.id.to_string(),
thread_id: beta_project.thread_id.to_string(),
file_hash: beta_version.files[0].hashes["sha1"].clone(),
},
alpha_project_slug: alpha_project.slug.unwrap(),
beta_project_slug: beta_project.slug.unwrap(),
organization_zeta: DummyOrganizationZeta {
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(),
beta_version_id: beta_version.id.to_string(),
pub async fn get_dummy_data(test_env: &TestEnvironment) -> DummyData {
let (alpha_project, alpha_version) = get_project_alpha(test_env).await;
let (beta_project, beta_version) = get_project_beta(test_env).await;
alpha_thread_id: alpha_project.thread_id.to_string(),
beta_thread_id: beta_project.thread_id.to_string(),
let zeta_organization = get_organization_zeta(test_env).await;
DummyData {
project_alpha: DummyProjectAlpha {
team_id: alpha_project.team.to_string(),
project_id: alpha_project.id.to_string(),
project_slug: alpha_project.slug.unwrap(),
version_id: alpha_version.id.to_string(),
thread_id: alpha_project.thread_id.to_string(),
file_hash: alpha_version.files[0].hashes["sha1"].clone(),
},
alpha_file_hash: alpha_version.files[0].hashes["sha1"].clone(),
beta_file_hash: beta_version.files[0].hashes["sha1"].clone(),
project_beta: DummyProjectBeta {
team_id: beta_project.team.to_string(),
project_id: beta_project.id.to_string(),
project_slug: beta_project.slug.unwrap(),
version_id: beta_version.id.to_string(),
thread_id: beta_project.thread_id.to_string(),
file_hash: beta_version.files[0].hashes["sha1"].clone(),
},
organization_zeta: DummyOrganizationZeta {
organization_id: zeta_organization.id.to_string(),
team_id: zeta_organization.team_id.to_string(),
organization_title: zeta_organization.title,
},
}
}
pub async fn add_project_alpha(test_env: &TestEnvironment) -> (Project, Version) {
test_env
let (project, versions) = test_env
.v2
.add_public_project(get_public_project_creation_data(
"alpha",
DummyJarFile::DummyProjectAlpha,
))
.await
.add_public_project(
get_public_project_creation_data("alpha", Some(DummyJarFile::DummyProjectAlpha)),
USER_USER_PAT,
)
.await;
(project, versions.into_iter().next().unwrap())
}
pub async fn add_project_beta(test_env: &TestEnvironment) -> (Project, Version) {
@@ -148,6 +234,48 @@ pub async fn add_project_beta(test_env: &TestEnvironment) -> (Project, Version)
assert_eq!(resp.status(), 200);
get_project_beta(test_env).await
}
pub async fn add_organization_zeta(test_env: &TestEnvironment) -> Organization {
// Add an organzation.
let req = TestRequest::post()
.uri("/v2/organization")
.append_header(("Authorization", USER_USER_PAT))
.set_json(json!({
"title": "zeta",
"description": "A dummy organization for testing with."
}))
.to_request();
let resp = test_env.call(req).await;
assert_eq!(resp.status(), 200);
get_organization_zeta(test_env).await
}
pub async fn get_project_alpha(test_env: &TestEnvironment) -> (Project, Version) {
// Get project
let req = TestRequest::get()
.uri("/v2/project/alpha")
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
let project: Project = test::read_body_json(resp).await;
// Get project's versions
let req = TestRequest::get()
.uri("/v2/project/alpha/version")
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
let versions: Vec<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
let req = TestRequest::get()
.uri("/v2/project/beta")
@@ -168,6 +296,18 @@ pub async fn add_project_beta(test_env: &TestEnvironment) -> (Project, Version)
(project, version)
}
pub async fn get_organization_zeta(test_env: &TestEnvironment) -> Organization {
// Get organization
let req = TestRequest::get()
.uri("/v2/organization/zeta")
.append_header(("Authorization", USER_USER_PAT))
.to_request();
let resp = test_env.call(req).await;
let organization: Organization = test::read_body_json(resp).await;
organization
}
impl DummyJarFile {
pub fn filename(&self) -> String {
match self {
@@ -194,3 +334,25 @@ impl DummyJarFile {
}
}
}
impl DummyImage {
pub fn filename(&self) -> String {
match self {
DummyImage::SmallIcon => "200x200.png",
}
.to_string()
}
pub fn extension(&self) -> String {
match self {
DummyImage::SmallIcon => "png",
}
.to_string()
}
pub fn bytes(&self) -> Vec<u8> {
match self {
DummyImage::SmallIcon => include_bytes!("../../tests/files/200x200.png").to_vec(),
}
}
}

View File

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

View File

@@ -11,11 +11,12 @@ pub mod database;
pub mod dummy_data;
pub mod environment;
pub mod pats;
pub mod permissions;
pub mod request_data;
pub mod scopes;
// Testing equivalent to 'setup' function, producing a LabrinthConfig
// If making a test, you should probably use environment::TestEnvironment::build_with_dummy() (which calls this)
// If making a test, you should probably use environment::TestEnvironment::build() (which calls this)
pub async fn setup(db: &TemporaryDatabase) -> LabrinthConfig {
println!("Setting up labrinth config");

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

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
-- These are not legal PATs, as they contain all scopes- they mimic permissions of a logged in user
-- IDs: 50-54, o p q r s
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (50, 1, 'admin-pat', 'mrp_patadmin', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00');
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (51, 2, 'moderator-pat', 'mrp_patmoderator', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00');
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (52, 3, 'user-pat', 'mrp_patuser', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00');
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (53, 4, 'friend-pat', 'mrp_patfriend', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00');
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (54, 5, 'enemy-pat', 'mrp_patenemy', B'11111111111111111111111111111111111'::BIGINT, '2030-08-18 15:48:58.435729+00');
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (50, 1, 'admin-pat', 'mrp_patadmin', $1, '2030-08-18 15:48:58.435729+00');
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (51, 2, 'moderator-pat', 'mrp_patmoderator', $1, '2030-08-18 15:48:58.435729+00');
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (52, 3, 'user-pat', 'mrp_patuser', $1, '2030-08-18 15:48:58.435729+00');
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (53, 4, 'friend-pat', 'mrp_patfriend', $1, '2030-08-18 15:48:58.435729+00');
INSERT INTO pats (id, user_id, name, access_token, scopes, expires) VALUES (54, 5, 'enemy-pat', 'mrp_patenemy', $1, '2030-08-18 15:48:58.435729+00');
-- -- Sample game versions, loaders, categories
INSERT INTO game_versions (id, version, type, created)
@@ -43,4 +43,9 @@ INSERT INTO categories (id, category, project_type) VALUES
(104, 'food', 2),
(105, 'magic', 2),
(106, 'mobs', 2),
(107, 'optimization', 2);
(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]
pub async fn get_user_notifications_after_team_invitation_returns_notification() {
with_test_environment(|test_env| async move {
let alpha_team_id = test_env.dummy.as_ref().unwrap().alpha_team_id.clone();
let alpha_team_id = test_env
.dummy
.as_ref()
.unwrap()
.project_alpha
.team_id
.clone();
let api = test_env.v2;
api.get_user_notifications_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT)
.await;
api.add_user_to_team(&alpha_team_id, FRIEND_USER_ID, USER_USER_PAT)
api.add_user_to_team(&alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
.await;
let notifications = api

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
#[actix_rt::test]
pub async fn pat_full_test() {
let test_env = TestEnvironment::build_with_dummy().await;
let test_env = TestEnvironment::build(None).await;
// Create a PAT for a full test
let req = test::TestRequest::post()
@@ -163,7 +163,7 @@ pub async fn pat_full_test() {
// Test illegal PAT setting, both in POST and PATCH
#[actix_rt::test]
pub async fn bad_pats() {
let test_env = TestEnvironment::build_with_dummy().await;
let test_env = TestEnvironment::build(None).await;
// Creating a PAT with no name should fail
let req = test::TestRequest::post()

File diff suppressed because it is too large Load Diff

View File

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

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;
// user GET (permissions, different users)
// users GET
// user auth
// user projects get
// user collections get
// patch user
// patch user icon
// user follows
#[actix_rt::test]
pub async fn get_user_projects_after_creating_project_returns_new_project() {
with_test_environment(|test_env| async move {
@@ -15,10 +24,10 @@ pub async fn get_user_projects_after_creating_project_returns_new_project() {
.await;
let (project, _) = api
.add_public_project(get_public_project_creation_data(
"slug",
DummyJarFile::BasicMod,
))
.add_public_project(
get_public_project_creation_data("slug", Some(DummyJarFile::BasicMod)),
USER_USER_PAT,
)
.await;
let resp_projects = api
@@ -34,15 +43,15 @@ pub async fn get_user_projects_after_deleting_project_shows_removal() {
with_test_environment(|test_env| async move {
let api = test_env.v2;
let (project, _) = api
.add_public_project(get_public_project_creation_data(
"iota",
DummyJarFile::BasicMod,
))
.add_public_project(
get_public_project_creation_data("iota", Some(DummyJarFile::BasicMod)),
USER_USER_PAT,
)
.await;
api.get_user_projects_deserialized(USER_USER_ID, USER_USER_PAT)
.await;
api.remove_project(&project.slug.as_ref().unwrap(), USER_USER_PAT)
api.remove_project(project.slug.as_ref().unwrap(), USER_USER_PAT)
.await;
let resp_projects = api
@@ -56,15 +65,15 @@ pub async fn get_user_projects_after_deleting_project_shows_removal() {
#[actix_rt::test]
pub async fn get_user_projects_after_joining_team_shows_team_projects() {
with_test_environment(|test_env| async move {
let alpha_team_id = &test_env.dummy.as_ref().unwrap().alpha_team_id;
let alpha_project_id = &test_env.dummy.as_ref().unwrap().alpha_project_id;
let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id;
let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id;
let api = test_env.v2;
api.get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT)
.await;
api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT)
api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
.await;
api.join_team(&alpha_team_id, FRIEND_USER_PAT).await;
api.join_team(alpha_team_id, FRIEND_USER_PAT).await;
let projects = api
.get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT)
@@ -79,16 +88,16 @@ pub async fn get_user_projects_after_joining_team_shows_team_projects() {
#[actix_rt::test]
pub async fn get_user_projects_after_leaving_team_shows_no_team_projects() {
with_test_environment(|test_env| async move {
let alpha_team_id = &test_env.dummy.as_ref().unwrap().alpha_team_id;
let alpha_project_id = &test_env.dummy.as_ref().unwrap().alpha_project_id;
let alpha_team_id = &test_env.dummy.as_ref().unwrap().project_alpha.team_id;
let alpha_project_id = &test_env.dummy.as_ref().unwrap().project_alpha.project_id;
let api = test_env.v2;
api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT)
api.add_user_to_team(alpha_team_id, FRIEND_USER_ID, None, None, USER_USER_PAT)
.await;
api.join_team(&alpha_team_id, FRIEND_USER_PAT).await;
api.join_team(alpha_team_id, FRIEND_USER_PAT).await;
api.get_user_projects_deserialized(FRIEND_USER_ID, FRIEND_USER_PAT)
.await;
api.remove_from_team(&alpha_team_id, FRIEND_USER_ID, USER_USER_PAT)
api.remove_from_team(alpha_team_id, FRIEND_USER_ID, USER_USER_PAT)
.await;
let projects = api