From d74bb2ec712c59cb12a773ba0d4a5431ce5631b5 Mon Sep 17 00:00:00 2001 From: paulobressan Date: Thu, 10 Oct 2024 19:06:30 -0300 Subject: [PATCH] feat: implemented delete pending user invite --- .gitignore | 1 + Cargo.lock | 2 +- src/domain/event/mod.rs | 24 ++++++++ src/domain/project/cache.rs | 9 +++ src/domain/project/command.rs | 113 +++++++++++++++++++++++++++++++++- src/driven/cache/project.rs | 34 ++++++++++ src/drivers/cache/mod.rs | 3 + src/drivers/grpc/project.rs | 20 ++++++ 8 files changed, 204 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index a4a5c2d..10798a8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ test/.terraform* test/local.tfstate* crds-path/ .github/iac/.terraform* +billing diff --git a/Cargo.lock b/Cargo.lock index b292f60..1fa8b4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1094,7 +1094,7 @@ dependencies = [ [[package]] name = "dmtri" version = "0.1.0" -source = "git+https://github.com/demeter-run/specs.git#24e84c82832280f7a28aadd9819393e98098aacd" +source = "git+https://github.com/demeter-run/specs.git#6f93355139c67522139af7c1f0923f9855a6ade5" dependencies = [ "bytes 1.7.2", "pbjson", diff --git a/src/domain/event/mod.rs b/src/domain/event/mod.rs index 2fab1f1..ce9b352 100644 --- a/src/domain/event/mod.rs +++ b/src/domain/event/mod.rs @@ -91,6 +91,15 @@ pub struct ProjectUserInviteAccepted { } into_event!(ProjectUserInviteAccepted); +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProjectUserInviteDeleted { + pub id: String, + pub project_id: String, + pub deleted_by: String, + pub deleted_at: DateTime, +} +into_event!(ProjectUserInviteDeleted); + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProjectUserDeleted { pub id: String, @@ -169,6 +178,7 @@ pub enum Event { ProjectSecretDeleted(ProjectSecretDeleted), ProjectUserInviteCreated(ProjectUserInviteCreated), ProjectUserInviteAccepted(ProjectUserInviteAccepted), + ProjectUserInviteDeleted(ProjectUserInviteDeleted), ProjectUserDeleted(ProjectUserDeleted), ResourceCreated(ResourceCreated), ResourceUpdated(ResourceUpdated), @@ -185,6 +195,7 @@ impl Event { Event::ProjectSecretDeleted(_) => "ProjectSecretDeleted".into(), Event::ProjectUserInviteCreated(_) => "ProjectUserInviteCreated".into(), Event::ProjectUserInviteAccepted(_) => "ProjectUserInviteAccepted".into(), + Event::ProjectUserInviteDeleted(_) => "ProjectUserInviteDeleted".into(), Event::ProjectUserDeleted(_) => "ProjectUserDeleted".into(), Event::ResourceCreated(_) => "ResourceCreated".into(), Event::ResourceUpdated(_) => "ResourceUpdated".into(), @@ -209,6 +220,9 @@ impl Event { "ProjectUserInviteAccepted" => Ok(Self::ProjectUserInviteAccepted( serde_json::from_slice(payload)?, )), + "ProjectUserInviteDeleted" => Ok(Self::ProjectUserInviteDeleted( + serde_json::from_slice(payload)?, + )), "ProjectUserDeleted" => Ok(Self::ProjectUserDeleted(serde_json::from_slice(payload)?)), "ResourceCreated" => Ok(Self::ResourceCreated(serde_json::from_slice(payload)?)), "ResourceUpdated" => Ok(Self::ResourceUpdated(serde_json::from_slice(payload)?)), @@ -304,6 +318,16 @@ mod tests { } } } + impl Default for ProjectUserInviteDeleted { + fn default() -> Self { + Self { + id: Uuid::new_v4().to_string(), + project_id: Uuid::new_v4().to_string(), + deleted_by: Uuid::new_v4().to_string(), + deleted_at: Utc::now(), + } + } + } impl Default for ResourceCreated { fn default() -> Self { Self { diff --git a/src/domain/project/cache.rs b/src/domain/project/cache.rs index 4e14ca5..563252b 100644 --- a/src/domain/project/cache.rs +++ b/src/domain/project/cache.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use crate::domain::event::{ ProjectCreated, ProjectDeleted, ProjectSecretCreated, ProjectSecretDeleted, ProjectUpdated, ProjectUserDeleted, ProjectUserInviteAccepted, ProjectUserInviteCreated, + ProjectUserInviteDeleted, }; use crate::domain::Result; @@ -43,6 +44,7 @@ pub trait ProjectDrivenCache: Send + Sync { async fn find_user_invite_by_code(&self, code: &str) -> Result>; async fn create_user_invite(&self, invite: &ProjectUserInvite) -> Result<()>; async fn create_user_acceptance(&self, invite_id: &str, user: &ProjectUser) -> Result<()>; + async fn delete_user_invite(&self, invite_id: &str) -> Result<()>; async fn delete_user(&self, project_id: &str, id: &str) -> Result<()>; } @@ -78,6 +80,13 @@ pub async fn create_user_invite( cache.create_user_invite(&evt.try_into()?).await } +pub async fn delete_user_invite( + cache: Arc, + evt: ProjectUserInviteDeleted, +) -> Result<()> { + cache.delete_user_invite(&evt.id.clone()).await +} + pub async fn create_user_invite_acceptance( cache: Arc, evt: ProjectUserInviteAccepted, diff --git a/src/domain/project/command.rs b/src/domain/project/command.rs index 59a61b9..04b15a6 100644 --- a/src/domain/project/command.rs +++ b/src/domain/project/command.rs @@ -15,7 +15,7 @@ use crate::domain::{ event::{ EventDrivenBridge, ProjectCreated, ProjectDeleted, ProjectSecretCreated, ProjectSecretDeleted, ProjectUpdated, ProjectUserDeleted, ProjectUserInviteAccepted, - ProjectUserInviteCreated, + ProjectUserInviteCreated, ProjectUserInviteDeleted, }, project::{ProjectStatus, ProjectUserAggregated, ProjectUserInviteStatus}, utils, Result, MAX_SECRET, PAGE_SIZE_DEFAULT, PAGE_SIZE_MAX, @@ -504,6 +504,42 @@ pub async fn resend_user_invite( Ok(()) } +pub async fn delete_user_invite( + cache: Arc, + event: Arc, + cmd: DeleteUserInviteCmd, +) -> Result<()> { + let user_id = assert_credential(&cmd.credential)?; + + let Some(user_invite) = cache.find_user_invite_by_id(&cmd.id).await? else { + return Err(Error::CommandMalformed("invalid invite id".into())); + }; + + assert_permission( + cache.clone(), + &cmd.credential, + &user_invite.project_id, + None, + ) + .await?; + + if user_invite.status == ProjectUserInviteStatus::Accepted { + return Err(Error::CommandMalformed("invite already accepted".into())); + } + + let evt = ProjectUserInviteDeleted { + id: cmd.id, + project_id: user_invite.project_id, + deleted_by: user_id, + deleted_at: Utc::now(), + }; + + event.dispatch(evt.into()).await?; + info!("project invite deleted"); + + Ok(()) +} + fn assert_credential(credential: &Credential) -> Result { match credential { Credential::Auth0(user_id) => Ok(user_id.into()), @@ -802,6 +838,17 @@ impl ResendUserInviteCmd { } } +#[derive(Debug, Clone)] +pub struct DeleteUserInviteCmd { + pub credential: Credential, + pub id: String, +} +impl DeleteUserInviteCmd { + pub fn new(credential: Credential, id: String) -> Self { + Self { credential, id } + } +} + #[cfg(test)] mod tests { use uuid::Uuid; @@ -935,6 +982,14 @@ mod tests { } } } + impl Default for DeleteUserInviteCmd { + fn default() -> Self { + Self { + credential: Credential::Auth0("user id".into()), + id: Uuid::new_v4().to_string(), + } + } + } impl Default for FetchUserCmd { fn default() -> Self { Self { @@ -1621,6 +1676,62 @@ mod tests { assert!(result.is_err()); } + #[tokio::test] + async fn it_should_delete_project_user_invite() { + let mut cache = MockProjectDrivenCache::new(); + cache + .expect_find_user_invite_by_id() + .return_once(|_| Ok(Some(ProjectUserInvite::default()))); + cache + .expect_find_user_permission() + .return_once(|_, _| Ok(Some(ProjectUser::default()))); + + let mut event = MockEventDrivenBridge::new(); + event.expect_dispatch().return_once(|_| Ok(())); + + let cmd = DeleteUserInviteCmd::default(); + + let result = delete_user_invite(Arc::new(cache), Arc::new(event), cmd).await; + + assert!(result.is_ok()); + } + #[tokio::test] + async fn it_should_fail_delete_project_user_invite_when_invite_doesnt_exist() { + let mut cache = MockProjectDrivenCache::new(); + cache + .expect_find_user_invite_by_id() + .return_once(|_| Ok(None)); + + let event = MockEventDrivenBridge::new(); + + let cmd = DeleteUserInviteCmd::default(); + + let result = delete_user_invite(Arc::new(cache), Arc::new(event), cmd).await; + + assert!(result.is_err()); + } + #[tokio::test] + async fn it_should_fail_delete_project_user_invite_when_invite_is_accepted() { + let mut cache = MockProjectDrivenCache::new(); + cache.expect_find_user_invite_by_id().return_once(|_| { + Ok(Some(ProjectUserInvite { + status: ProjectUserInviteStatus::Accepted, + ..Default::default() + })) + }); + cache + .expect_find_user_permission() + .return_once(|_, _| Ok(Some(ProjectUser::default()))); + + let event = MockEventDrivenBridge::new(); + + let cmd = DeleteUserInviteCmd::default(); + + let result = delete_user_invite(Arc::new(cache), Arc::new(event), cmd).await; + + assert!(result.is_err()); + } + #[tokio::test] async fn it_should_fetch_project_users() { let mut cache = MockProjectDrivenCache::new(); diff --git a/src/driven/cache/project.rs b/src/driven/cache/project.rs index baaeb4c..a65d843 100644 --- a/src/driven/cache/project.rs +++ b/src/driven/cache/project.rs @@ -554,6 +554,22 @@ impl ProjectDrivenCache for SqliteProjectDrivenCache { Ok(()) } + async fn delete_user_invite(&self, invite_id: &str) -> Result<()> { + sqlx::query!( + r#" + DELETE FROM + project_user_invite + WHERE + id=$1; + "#, + invite_id, + ) + .execute(&self.sqlite.db) + .await?; + + Ok(()) + } + async fn delete_user(&self, project_id: &str, id: &str) -> Result<()> { sqlx::query!( r#" @@ -957,6 +973,24 @@ mod tests { assert!(result.is_ok()); } + #[tokio::test] + async fn it_should_delete_user_invite() { + let cache = get_cache().await; + + let project = Project::default(); + cache.create(&project).await.unwrap(); + + let invite = ProjectUserInvite { + project_id: project.id.clone(), + ..Default::default() + }; + cache.create_user_invite(&invite).await.unwrap(); + + let result = cache.delete_user_invite(&invite.id).await; + + assert!(result.is_ok()); + } + #[tokio::test] async fn it_should_find_users() { let cache = get_cache().await; diff --git a/src/drivers/cache/mod.rs b/src/drivers/cache/mod.rs index 86bffba..6d393b0 100644 --- a/src/drivers/cache/mod.rs +++ b/src/drivers/cache/mod.rs @@ -93,6 +93,9 @@ pub async fn subscribe(config: CacheConfig) -> Result<()> { ) .await } + Event::ProjectUserInviteDeleted(evt) => { + project::cache::delete_user_invite(project_cache.clone(), evt.clone()).await + } Event::ProjectUserDeleted(evt) => { project::cache::delete_user(project_cache.clone(), evt.clone()).await } diff --git a/src/drivers/grpc/project.rs b/src/drivers/grpc/project.rs index 5b2f6e2..c1d20f7 100644 --- a/src/drivers/grpc/project.rs +++ b/src/drivers/grpc/project.rs @@ -384,6 +384,26 @@ impl proto::project_service_server::ProjectService for ProjectServiceImpl { Ok(tonic::Response::new(message)) } + async fn delete_project_user_invite( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let credential = match request.extensions().get::() { + Some(credential) => credential.clone(), + None => return Err(Status::unauthenticated("invalid credential")), + }; + + let req = request.into_inner(); + + let cmd = project::command::DeleteUserInviteCmd::new(credential, req.id); + + project::command::delete_user_invite(self.cache.clone(), self.event.clone(), cmd.clone()) + .await?; + + let message = proto::DeleteProjectUserInviteResponse {}; + + Ok(tonic::Response::new(message)) + } async fn delete_project_user( &self,