Skip to content

Commit

Permalink
Implemented delete pending user invite (#168)
Browse files Browse the repository at this point in the history
* feat: implemented delete pending user invite

* chore: updated sqlx files
  • Loading branch information
paulobressan authored Oct 14, 2024
1 parent 5a454de commit 3e23ec4
Show file tree
Hide file tree
Showing 9 changed files with 216 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ test/.terraform*
test/local.tfstate*
crds-path/
.github/iac/.terraform*
billing

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions src/domain/event/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Utc>,
}
into_event!(ProjectUserInviteDeleted);

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectUserDeleted {
pub id: String,
Expand Down Expand Up @@ -169,6 +178,7 @@ pub enum Event {
ProjectSecretDeleted(ProjectSecretDeleted),
ProjectUserInviteCreated(ProjectUserInviteCreated),
ProjectUserInviteAccepted(ProjectUserInviteAccepted),
ProjectUserInviteDeleted(ProjectUserInviteDeleted),
ProjectUserDeleted(ProjectUserDeleted),
ResourceCreated(ResourceCreated),
ResourceUpdated(ResourceUpdated),
Expand All @@ -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(),
Expand All @@ -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)?)),
Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 9 additions & 0 deletions src/domain/project/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -43,6 +44,7 @@ pub trait ProjectDrivenCache: Send + Sync {
async fn find_user_invite_by_code(&self, code: &str) -> Result<Option<ProjectUserInvite>>;
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<()>;
}

Expand Down Expand Up @@ -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<dyn ProjectDrivenCache>,
evt: ProjectUserInviteDeleted,
) -> Result<()> {
cache.delete_user_invite(&evt.id.clone()).await
}

pub async fn create_user_invite_acceptance(
cache: Arc<dyn ProjectDrivenCache>,
evt: ProjectUserInviteAccepted,
Expand Down
113 changes: 112 additions & 1 deletion src/domain/project/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -504,6 +504,42 @@ pub async fn resend_user_invite(
Ok(())
}

pub async fn delete_user_invite(
cache: Arc<dyn ProjectDrivenCache>,
event: Arc<dyn EventDrivenBridge>,
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<UserId> {
match credential {
Credential::Auth0(user_id) => Ok(user_id.into()),
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand Down
34 changes: 34 additions & 0 deletions src/driven/cache/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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#"
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/drivers/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
20 changes: 20 additions & 0 deletions src/drivers/grpc/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<proto::DeleteProjectUserInviteRequest>,
) -> Result<tonic::Response<proto::DeleteProjectUserInviteResponse>, tonic::Status> {
let credential = match request.extensions().get::<Credential>() {
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,
Expand Down

0 comments on commit 3e23ec4

Please sign in to comment.