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

@@ -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(),
}
}