diff --git a/.sqlx/query-0be48b74255947b8550eb98dfba0bafd0038147973cf7c9a2c4d0bd33f4776ab.json b/.sqlx/query-0be48b74255947b8550eb98dfba0bafd0038147973cf7c9a2c4d0bd33f4776ab.json new file mode 100644 index 00000000..93e9d929 --- /dev/null +++ b/.sqlx/query-0be48b74255947b8550eb98dfba0bafd0038147973cf7c9a2c4d0bd33f4776ab.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT user_id\n FROM team_members\n WHERE (team_id = $1 AND role = $2)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "user_id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "0be48b74255947b8550eb98dfba0bafd0038147973cf7c9a2c4d0bd33f4776ab" +} diff --git a/.sqlx/query-b36877d60945eaae76680770a5d28d2cbb26cfbb0ec94ecc8f0741f48178ec1c.json b/.sqlx/query-fbfa35f79abe9662cfbad62db8ecd7e38197123e2af364dd7d3b01c322345d7b.json similarity index 63% rename from .sqlx/query-b36877d60945eaae76680770a5d28d2cbb26cfbb0ec94ecc8f0741f48178ec1c.json rename to .sqlx/query-fbfa35f79abe9662cfbad62db8ecd7e38197123e2af364dd7d3b01c322345d7b.json index 74091817..a598df51 100644 --- a/.sqlx/query-b36877d60945eaae76680770a5d28d2cbb26cfbb0ec94ecc8f0741f48178ec1c.json +++ b/.sqlx/query-fbfa35f79abe9662cfbad62db8ecd7e38197123e2af364dd7d3b01c322345d7b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO mods (\n id, team_id, title, description, body,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status, requested_status, discord_url,\n client_side, server_side, license_url, license,\n slug, project_type, color, monetization_status\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13, $14,\n $15, $16, $17, $18,\n LOWER($19), $20, $21, $22\n )\n ", + "query": "\n INSERT INTO mods (\n id, team_id, title, description, body,\n published, downloads, icon_url, issues_url,\n source_url, wiki_url, status, requested_status, discord_url,\n client_side, server_side, license_url, license,\n slug, project_type, color, monetization_status,\n organization_id\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7, $8, $9,\n $10, $11, $12, $13, $14,\n $15, $16, $17, $18,\n LOWER($19), $20, $21, $22,\n $23\n )\n ", "describe": { "columns": [], "parameters": { @@ -26,10 +26,11 @@ "Text", "Int4", "Int4", - "Varchar" + "Varchar", + "Int8" ] }, "nullable": [] }, - "hash": "b36877d60945eaae76680770a5d28d2cbb26cfbb0ec94ecc8f0741f48178ec1c" + "hash": "fbfa35f79abe9662cfbad62db8ecd7e38197123e2af364dd7d3b01c322345d7b" } diff --git a/src/database/models/event_item.rs b/src/database/models/event_item.rs index caf0c51e..567d3934 100644 --- a/src/database/models/event_item.rs +++ b/src/database/models/event_item.rs @@ -7,11 +7,11 @@ use itertools::Itertools; use sqlx::postgres::{PgHasArrayType, PgTypeInfo}; use std::convert::{TryFrom, TryInto}; -#[derive(sqlx::Type, Clone, Copy)] +#[derive(sqlx::Type, Clone, Copy, Debug)] #[sqlx(type_name = "text")] #[sqlx(rename_all = "snake_case")] pub enum EventType { - ProjectCreated, + ProjectPublished, VersionCreated, ProjectUpdated, } @@ -30,7 +30,7 @@ pub enum CreatorId { #[derive(Debug)] pub enum EventData { - ProjectCreated { + ProjectPublished { project_id: ProjectId, creator_id: CreatorId, }, @@ -104,7 +104,7 @@ impl TryFrom for CreatorId { impl From for RawEvent { fn from(value: Event) -> Self { match value.event_data { - EventData::ProjectCreated { + EventData::ProjectPublished { project_id, creator_id, } => { @@ -116,7 +116,7 @@ impl From for RawEvent { target_id_type: target_id.id_type, triggerer_id: Some(triggerer_id.id), triggerer_id_type: Some(triggerer_id.id_type), - event_type: EventType::ProjectCreated, + event_type: EventType::ProjectPublished, metadata: None, created: None, } @@ -175,11 +175,11 @@ impl TryFrom for Event { let event = Event { id : value.id, event_data : match value.event_type { - EventType::ProjectCreated => EventData::ProjectCreated { + EventType::ProjectPublished => EventData::ProjectPublished { project_id: target_id.try_into()?, creator_id: triggerer_id.map_or_else(|| { Err(DatabaseError::UnexpectedNull( - "Neither triggerer_id nor triggerer_id_type should be null for project creation".to_string(), + "Neither triggerer_id nor triggerer_id_type should be null for project publishing".to_string(), )) }, |v| v.try_into())?, }, @@ -242,7 +242,8 @@ impl Event { unzip_event_selectors(target_selectors); let (triggerer_ids, triggerer_id_types, triggerer_event_types) = unzip_event_selectors(triggerer_selectors); - sqlx::query_as!( + + let r = sqlx::query_as!( RawEvent, r#" SELECT @@ -274,7 +275,9 @@ impl Event { .await? .into_iter() .map(|r| r.try_into()) - .collect::, _>>() + .collect::, _>>()?; + + Ok(r) } } diff --git a/src/database/models/project_item.rs b/src/database/models/project_item.rs index a3fddec2..a8ee1c44 100644 --- a/src/database/models/project_item.rs +++ b/src/database/models/project_item.rs @@ -299,14 +299,16 @@ impl Project { published, downloads, icon_url, issues_url, source_url, wiki_url, status, requested_status, discord_url, client_side, server_side, license_url, license, - slug, project_type, color, monetization_status + slug, project_type, color, monetization_status, + organization_id ) VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, - LOWER($19), $20, $21, $22 + LOWER($19), $20, $21, $22, + $23 ) ", self.id as ProjectId, @@ -331,6 +333,7 @@ impl Project { self.project_type as ProjectTypeId, self.color.map(|x| x as i32), self.monetization_status.as_str(), + self.organization_id.map(|x| x.0), ) .execute(&mut **transaction) .await?; diff --git a/src/database/models/team_item.rs b/src/database/models/team_item.rs index a513aefe..df59c623 100644 --- a/src/database/models/team_item.rs +++ b/src/database/models/team_item.rs @@ -390,6 +390,32 @@ impl TeamMember { } } + pub async fn get_owner_id<'a, 'b, E>( + id: TeamId, + executor: E, + ) -> Result, super::DatabaseError> + where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, + { + let result = sqlx::query!( + " + SELECT user_id + FROM team_members + WHERE (team_id = $1 AND role = $2) + ", + id as TeamId, + crate::models::teams::OWNER_ROLE, + ) + .fetch_optional(executor) + .await?; + + if let Some(m) = result { + Ok(Some(UserId(m.user_id))) + } else { + Ok(None) + } + } + pub async fn insert( &self, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, diff --git a/src/models/feeds.rs b/src/models/feeds.rs index 1d3fa387..be3fdb5b 100644 --- a/src/models/feeds.rs +++ b/src/models/feeds.rs @@ -29,7 +29,7 @@ pub struct FeedItem { #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type", rename_all = "snake_case")] pub enum FeedItemBody { - ProjectCreated { + ProjectPublished { project_id: ProjectId, creator_id: CreatorId, project_title: String, diff --git a/src/routes/v2/project_creation.rs b/src/routes/v2/project_creation.rs index 91751c37..ea287745 100644 --- a/src/routes/v2/project_creation.rs +++ b/src/routes/v2/project_creation.rs @@ -1,6 +1,5 @@ use super::version_creation::InitialVersionData; use crate::auth::{get_user_from_headers, AuthenticationError}; -use crate::database::models::event_item::{CreatorId, Event, EventData}; use crate::database::models::thread_item::ThreadBuilder; use crate::database::models::{self, image_item, User}; use crate::database::redis::RedisPool; @@ -14,7 +13,7 @@ use crate::models::projects::{ DonationLink, License, MonetizationStatus, ProjectId, ProjectStatus, SideType, VersionId, VersionStatus, }; -use crate::models::teams::ProjectPermissions; +use crate::models::teams::{OrganizationPermissions, ProjectPermissions}; use crate::models::threads::ThreadType; use crate::models::users::UserId; use crate::queue::session::AuthQueue; @@ -453,6 +452,29 @@ async fn project_create_inner( } } + // If organization_id is set, make sure the user is a member of the organization + if let Some(organization_id) = create_data.organization_id { + let organization_team_member = + models::team_item::TeamMember::get_from_user_id_organization( + organization_id.into(), + current_user.id.into(), + &mut **transaction, + ) + .await?; + + let permissions = OrganizationPermissions::get_permissions_by_role( + ¤t_user.role, + &organization_team_member, + ) + .unwrap_or_default(); + + if !permissions.contains(OrganizationPermissions::ADD_PROJECT) { + return Err(CreateError::CustomAuthenticationError( + "You do not have permission to add projects to this organization!".to_string(), + )); + } + } + // Create VersionBuilders for the versions specified in `initial_versions` versions = Vec::with_capacity(create_data.initial_versions.len()); for (i, data) in create_data.initial_versions.iter().enumerate() { @@ -749,9 +771,6 @@ async fn project_create_inner( } let organization_id = project_create_data.organization_id.map(|id| id.into()); - insert_project_create_event(project_id, organization_id, ¤t_user, transaction) - .await?; - let project_builder_actual = models::project_item::ProjectBuilder { project_id: project_id.into(), project_type_id, @@ -893,27 +912,6 @@ async fn project_create_inner( } } -async fn insert_project_create_event( - project_id: ProjectId, - organization_id: Option, - current_user: &crate::models::users::User, - transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, -) -> Result<(), CreateError> { - let event = Event::new( - EventData::ProjectCreated { - project_id: project_id.into(), - creator_id: organization_id.map_or_else( - || CreatorId::User(current_user.id.into()), - CreatorId::Organization, - ), - }, - transaction, - ) - .await?; - event.insert(transaction).await?; - Ok(()) -} - async fn create_initial_version( version_data: &InitialVersionData, project_id: ProjectId, diff --git a/src/routes/v2/projects.rs b/src/routes/v2/projects.rs index 876a7e1a..29b25683 100644 --- a/src/routes/v2/projects.rs +++ b/src/routes/v2/projects.rs @@ -4,7 +4,7 @@ use crate::database::models::event_item::{CreatorId, EventData}; use crate::database::models::notification_item::NotificationBuilder; use crate::database::models::project_item::{GalleryItem, ModCategory}; use crate::database::models::thread_item::ThreadMessageBuilder; -use crate::database::models::{image_item, Event}; +use crate::database::models::{image_item, team_item, Event}; use crate::database::redis::RedisPool; use crate::file_hosting::FileHost; use crate::models; @@ -25,13 +25,14 @@ use crate::util::routes::read_from_payload; use crate::util::validate::validation_errors_to_string; use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse}; use chrono::{DateTime, Utc}; -use db_ids::OrganizationId; +use db_ids::{OrganizationId, UserId}; use futures::TryStreamExt; use meilisearch_sdk::indexes::IndexesResults; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::PgPool; use std::sync::Arc; +use team_item::TeamMember; use validator::Validate; use database::models as db_models; @@ -528,6 +529,21 @@ pub async fn project_edit( ) .execute(&mut *transaction) .await?; + + // On publish event, we send out notification using team *owner* as publishing user, at the time of mod approval + // (even though 'user' is the mod and doing the publishing) + let owner_id = + TeamMember::get_owner_id(project_item.inner.team_id, &mut *transaction) + .await?; + if let Some(owner_id) = owner_id { + insert_project_publish_event( + id.into(), + project_item.inner.organization_id, + owner_id, + &mut transaction, + ) + .await?; + } } if status.is_searchable() && !project_item.inner.webhook_sent { @@ -1120,13 +1136,15 @@ pub async fn project_edit( ) .await?; - insert_project_update_event( - id.into(), - project_item.inner.organization_id, - &user, - &mut transaction, - ) - .await?; + if project_item.inner.status.is_searchable() { + insert_project_update_event( + project_item.inner.id.into(), + project_item.inner.organization_id, + &user, + &mut transaction, + ) + .await?; + } transaction.commit().await?; Ok(HttpResponse::NoContent().body("")) @@ -1477,13 +1495,15 @@ pub async fn projects_edit( .await?; } - insert_project_update_event( - project.inner.id.into(), - project.inner.organization_id, - &user, - &mut transaction, - ) - .await?; + if project.inner.status.is_searchable() { + insert_project_update_event( + project.inner.id.into(), + project.inner.organization_id, + &user, + &mut transaction, + ) + .await?; + } db_models::Project::clear_cache(project.inner.id, project.inner.slug, None, &redis).await?; } @@ -2597,3 +2617,22 @@ async fn insert_project_update_event( event.insert(transaction).await?; Ok(()) } + +async fn insert_project_publish_event( + project_id: ProjectId, + organization_id: Option, + owner_id: UserId, + transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result<(), ApiError> { + let event = Event::new( + EventData::ProjectPublished { + project_id: project_id.into(), + creator_id: organization_id + .map_or_else(|| CreatorId::User(owner_id), CreatorId::Organization), + }, + transaction, + ) + .await?; + event.insert(transaction).await?; + Ok(()) +} diff --git a/src/routes/v2/version_creation.rs b/src/routes/v2/version_creation.rs index 83da5a0b..fef61899 100644 --- a/src/routes/v2/version_creation.rs +++ b/src/routes/v2/version_creation.rs @@ -977,7 +977,6 @@ async fn insert_version_create_event( current_user: &crate::models::users::User, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result<(), CreateError> { - println!("Adding version create event"); let event = Event::new( EventData::VersionCreated { version_id: version_id.into(), diff --git a/src/routes/v3/users.rs b/src/routes/v3/users.rs index 1e459dea..8593540e 100644 --- a/src/routes/v3/users.rs +++ b/src/routes/v3/users.rs @@ -146,7 +146,7 @@ pub async fn current_user_feed( // - Projects updated by users you follow // - Projects updated by organizations you follow let event_types = [ - EventType::ProjectCreated, + EventType::ProjectPublished, EventType::VersionCreated, EventType::ProjectUpdated, ]; @@ -172,8 +172,6 @@ pub async fn current_user_feed( .take(params.offset.unwrap_or(usize::MAX)) .collect_vec(); - println!("ALL EVENTS: {:#?}", events); - let mut feed_items: Vec = Vec::new(); let authorized_versions = prefetch_authorized_event_versions(&events, &pool, &redis, ¤t_user).await?; @@ -190,22 +188,14 @@ pub async fn current_user_feed( ) .await?; - println!( - "All authorized versoins: {:#?}", - serde_json::to_string(&authorized_versions).unwrap() - ); - println!( - "All authorized projects: {:#?}", - serde_json::to_string(&authorized_projects).unwrap() - ); for event in events { let body = match event.event_data { - EventData::ProjectCreated { + EventData::ProjectPublished { project_id, creator_id, } => authorized_projects.get(&project_id.into()).map(|p| { - FeedItemBody::ProjectCreated { + FeedItemBody::ProjectPublished { project_id: project_id.into(), creator_id: creator_id.into(), project_title: p.title.clone(), @@ -225,18 +215,9 @@ pub async fn current_user_feed( version_id, creator_id, } => { - println!("Making version ev"); let authorized_version = authorized_versions.get(&version_id.into()); let authorized_project = authorized_version.and_then(|v| authorized_projects.get(&v.project_id)); - println!( - "av: {:#?}", - serde_json::to_string(&authorized_version).unwrap() - ); - println!( - "ap: {:#?}", - serde_json::to_string(&authorized_project).unwrap() - ); if let (Some(authorized_version), Some(authorized_project)) = (authorized_version, authorized_project) { @@ -276,7 +257,7 @@ async fn prefetch_authorized_event_projects( let mut project_ids = events .iter() .filter_map(|e| match &e.event_data { - EventData::ProjectCreated { + EventData::ProjectPublished { project_id, creator_id: _, } => Some(*project_id), @@ -316,7 +297,7 @@ async fn prefetch_authorized_event_versions( version_id, creator_id: _, } => Some(*version_id), - EventData::ProjectCreated { .. } => None, + EventData::ProjectPublished { .. } => None, EventData::ProjectUpdated { .. } => None, }) .collect_vec(); diff --git a/tests/common/asserts.rs b/tests/common/asserts.rs index 0137d7aa..131cadba 100644 --- a/tests/common/asserts.rs +++ b/tests/common/asserts.rs @@ -31,5 +31,17 @@ pub fn assert_feed_contains_project_created( feed: &[FeedItem], expected_project_id: labrinth::models::projects::ProjectId, ) { - assert!(feed.iter().any(|fi| matches!(fi.body, FeedItemBody::ProjectCreated { project_id, .. } if project_id == expected_project_id)), "{:#?}", &feed); + assert!(feed.iter().any(|fi| matches!(fi.body, FeedItemBody::ProjectPublished { project_id, .. } if project_id == expected_project_id)), "{:#?}", &feed); +} +pub fn assert_feed_contains_project_updated( + feed: &[FeedItem], + expected_project_id: labrinth::models::projects::ProjectId, +) { + assert!(feed.iter().any(|fi| matches!(fi.body, FeedItemBody::ProjectUpdated { project_id, .. } if project_id == expected_project_id)), "{:#?}", &feed); +} +pub fn assert_feed_contains_version_created( + feed: &[FeedItem], + expected_version_id: labrinth::models::projects::VersionId, +) { + assert!(feed.iter().any(|fi| matches!(fi.body, FeedItemBody::VersionCreated { version_id, .. } if version_id == expected_version_id)), "{:#?}", &feed); } diff --git a/tests/feed.rs b/tests/feed.rs index 134eed3a..a8f52a65 100644 --- a/tests/feed.rs +++ b/tests/feed.rs @@ -1,10 +1,16 @@ -use crate::common::{asserts::assert_feed_contains_project_created, dummy_data::DummyProjectAlpha}; +use crate::common::{ + asserts::{ + assert_feed_contains_project_created, assert_feed_contains_project_updated, + assert_feed_contains_version_created, + }, + dummy_data::DummyProjectAlpha, +}; use assert_matches::assert_matches; use common::{ database::{FRIEND_USER_PAT, USER_USER_ID, USER_USER_PAT}, environment::with_test_environment, }; -use labrinth::models::feeds::FeedItemBody; +use labrinth::models::{feeds::FeedItemBody, ids::base62_impl::parse_base62, projects::ProjectId}; mod common; @@ -30,10 +36,10 @@ async fn get_feed_after_following_user_shows_previously_created_public_projects( let feed = env.v3.get_feed(FRIEND_USER_PAT).await; assert_eq!(feed.len(), 1); - assert_matches!( - feed[0].body, - FeedItemBody::ProjectCreated { project_id, .. } if project_id.to_string() == alpha_project_id - ) + assert_feed_contains_project_created( + &feed, + ProjectId(parse_base62(&alpha_project_id).unwrap()), + ); }) .await } @@ -47,7 +53,9 @@ async fn get_feed_after_following_user_shows_previously_created_public_versions( } = env.dummy.as_ref().unwrap().project_alpha.clone(); // Add version - let v = env.v2.create_default_version(&alpha_project_id, None, USER_USER_PAT) + let v = env + .v2 + .create_default_version(&alpha_project_id, None, USER_USER_PAT) .await; env.v3.follow_user(USER_USER_ID, FRIEND_USER_PAT).await; @@ -55,20 +63,17 @@ async fn get_feed_after_following_user_shows_previously_created_public_versions( let feed = env.v3.get_feed(FRIEND_USER_PAT).await; assert_eq!(feed.len(), 2); - assert_matches!( - feed[1].body, - FeedItemBody::ProjectCreated { project_id, .. } if project_id.to_string() == alpha_project_id - ); - assert_matches!( - feed[0].body, - FeedItemBody::VersionCreated { version_id, .. } if version_id == v.id + assert_feed_contains_project_created( + &feed, + ProjectId(parse_base62(&alpha_project_id).unwrap()), ); + assert_feed_contains_version_created(&feed, v.id); + // Notably, this should *not* have a projectupdated from the publishing. }) .await } #[actix_rt::test] - async fn get_feed_after_following_user_shows_previously_edited_public_versions() { with_test_environment(|env| async move { let DummyProjectAlpha { @@ -77,7 +82,8 @@ async fn get_feed_after_following_user_shows_previously_edited_public_versions() } = env.dummy.as_ref().unwrap().project_alpha.clone(); // Empty patch - env.v2.edit_project(&alpha_project_id, serde_json::json!({}), USER_USER_PAT) + env.v2 + .edit_project(&alpha_project_id, serde_json::json!({}), USER_USER_PAT) .await; env.v3.follow_user(USER_USER_ID, FRIEND_USER_PAT).await; @@ -85,13 +91,13 @@ async fn get_feed_after_following_user_shows_previously_edited_public_versions() let feed = env.v3.get_feed(FRIEND_USER_PAT).await; assert_eq!(feed.len(), 2); - assert_matches!( - feed[1].body, - FeedItemBody::ProjectCreated { project_id, .. } if project_id.to_string() == alpha_project_id + assert_feed_contains_project_created( + &feed, + ProjectId(parse_base62(&alpha_project_id).unwrap()), ); - assert_matches!( - feed[0].body, - FeedItemBody::ProjectUpdated { project_id, .. } if project_id.to_string() == alpha_project_id + assert_feed_contains_project_updated( + &feed, + ProjectId(parse_base62(&alpha_project_id).unwrap()), ); }) .await @@ -107,7 +113,8 @@ async fn get_feed_when_following_user_that_creates_project_as_org_only_shows_eve env.v3.follow_user(USER_USER_ID, FRIEND_USER_PAT).await; let feed = env.v3.get_feed(FRIEND_USER_PAT).await; assert_eq!(feed.len(), 1); - assert_matches!(feed[0].body, FeedItemBody::ProjectCreated { project_id, .. } if project_id != project.id); + + assert_matches!(feed[0].body, FeedItemBody::ProjectPublished { project_id, .. } if project_id != project.id); env.v3.follow_organization(&org_id, FRIEND_USER_PAT).await; let feed = env.v3.get_feed(FRIEND_USER_PAT).await; diff --git a/tests/organizations.rs b/tests/organizations.rs index edbb84bd..f427c22f 100644 --- a/tests/organizations.rs +++ b/tests/organizations.rs @@ -7,8 +7,10 @@ use crate::common::{ use actix_web::test; use bytes::Bytes; use common::{ + actix::AppendsMultipart, database::{FRIEND_USER_ID, FRIEND_USER_PAT, USER_USER_PAT}, permissions::{PermissionsTest, PermissionsTestContext}, + request_data::get_public_project_creation_data, }; use labrinth::models::teams::{OrganizationPermissions, ProjectPermissions}; use serde_json::json; @@ -289,6 +291,75 @@ async fn add_remove_organization_projects() { test_env.cleanup().await; } +#[actix_rt::test] +async fn create_project_in_organization() { + let test_env = TestEnvironment::build(None).await; + let zeta_organization_id: &str = &test_env + .dummy + .as_ref() + .unwrap() + .organization_zeta + .organization_id; + + // Create project in organization + let resp = test_env + .v2 + .add_default_org_project(zeta_organization_id, USER_USER_PAT) + .await; + + // Get project + let project = test_env + .v2 + .get_project_deserialized(&resp.id.to_string(), USER_USER_PAT) + .await; + + // Ensure organization id is correectly set in both returned project and + // fetched project. + assert_eq!(resp.organization.unwrap().to_string(), zeta_organization_id); + assert_eq!( + project.organization.unwrap().to_string(), + zeta_organization_id + ); + + test_env.cleanup().await; +} + +#[actix_rt::test] +async fn permissions_create_project_in_organization() { + 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; + + // Requires ADD_PROJECT to create project in org + let add_project = OrganizationPermissions::ADD_PROJECT; + + let req_gen = |ctx: &PermissionsTestContext| { + let multipart = get_public_project_creation_data( + &generate_random_name("randomslug"), + None, + Some(ctx.organization_id.unwrap()), + ) + .segment_data; + test::TestRequest::post() + .uri("/v2/project") + .set_multipart(multipart) + }; + 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(); + + test_env.cleanup().await; +} + #[actix_rt::test] async fn permissions_patch_organization() { let test_env = TestEnvironment::build(Some(8)).await;