feat: workspace manager & test (#117)

* chore: add workspace tests

* chore: add slqx files

* feat: update workspace member role

* chore: update
This commit is contained in:
Nathan.fooo 2023-10-14 12:23:23 +08:00 committed by GitHub
parent 5c58f95f9f
commit 3e73adc82d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 496 additions and 171 deletions

View file

@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT EXISTS(\n SELECT 1\n FROM public.af_workspace_member\n JOIN af_roles ON af_workspace_member.role_id = af_roles.id\n WHERE workspace_id = $1\n AND af_workspace_member.uid = (\n SELECT uid FROM public.af_user WHERE uuid = $2\n )\n AND af_roles.name = 'Owner'\n ) AS \"exists\";\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "36733444fc8fac851fb540105ea6c9dca785455ae44ae518b98d8b57082e11d8"
}

View file

@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE af_workspace_member\n SET \n role_id = COALESCE($1, role_id)\n WHERE workspace_id = $2 AND uid = (\n SELECT uid FROM af_user WHERE email = $3\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "4162ec00fad0abe726492a5b916205eec1f004bcf71864dd59c84fad6f3b7e98"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM public.af_workspace_member\n WHERE \n workspace_id = $1 \n AND uid = (\n SELECT uid FROM public.af_user WHERE email = $2\n )\n -- Ensure the user to be deleted is not the original owner\n AND uid <> (\n SELECT owner_uid FROM public.af_workspace WHERE workspace_id = $1\n );\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": []
},
"hash": "54c2e86c48a1549d8006bd5592eb5610985d5d43cd67efd417a0c6e52a967dfd"
}

View file

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM public.af_workspace_member\n WHERE workspace_id = $1 AND uid IN (\n SELECT uid FROM public.af_user WHERE email = ANY($2::text[])\n )\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"TextArray"
]
},
"nullable": []
},
"hash": "a8bafeabd18222d562ae65a6bb56a55615d4e2dbf93db484378f470b529ac08a"
}

View file

@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT EXISTS(\n SELECT 1 FROM public.af_workspace\n WHERE workspace_id = $1 AND owner_uid = (\n SELECT uid FROM public.af_user WHERE uuid = $2\n )\n )\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "exists",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": [
null
]
},
"hash": "ede9205510d7deac87d47a56d65235b1c930d2b6e0765176e4e30935cc347299"
}

View file

@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT EXISTS (\n SELECT 1 \n FROM public.af_workspace\n WHERE \n workspace_id = $1 \n AND owner_uid = (\n SELECT uid FROM public.af_user WHERE email = $2\n )\n ) AS \"is_owner\";\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "is_owner",
"type_info": "Bool"
}
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
},
"nullable": [
null
]
},
"hash": "fa7a89a789e010e29dd4737b5614ebb750e446a4c76e298731b1801dee679278"
}

View file

@ -23,16 +23,19 @@ use reqwest::RequestBuilder;
use scraper::{Html, Selector};
use shared_entity::app_error::AppError;
use shared_entity::data::AppResponse;
use shared_entity::dto::SignInTokenResponse;
use shared_entity::dto::UpdateUsernameParams;
use shared_entity::dto::UserUpdateParams;
use shared_entity::dto::{CreateWorkspaceMembers, WorkspaceMembers};
use shared_entity::dto::auth_dto::SignInTokenResponse;
use shared_entity::dto::auth_dto::UpdateUsernameParams;
use shared_entity::dto::auth_dto::UserUpdateParams;
use shared_entity::dto::workspace_dto::{
CreateWorkspaceMembers, WorkspaceMemberChangeset, WorkspaceMembers,
};
use shared_entity::error_code::url_missing_param;
use shared_entity::error_code::ErrorCode;
use std::sync::Arc;
use std::time::SystemTime;
use tracing::instrument;
use url::Url;
use uuid::Uuid;
/// `Client` is responsible for managing communication with the GoTrue API and cloud storage.
///
@ -360,7 +363,7 @@ impl Client {
#[instrument(level = "debug", skip_all, err)]
pub async fn get_workspace_members(
&self,
workspace_uuid: uuid::Uuid,
workspace_uuid: Uuid,
) -> Result<Vec<AFWorkspaceMember>, AppError> {
let url = format!("{}/api/workspace/{}/member", self.base_url, workspace_uuid);
let resp = self
@ -376,7 +379,7 @@ impl Client {
#[instrument(level = "debug", skip_all, err)]
pub async fn add_workspace_members<T: Into<CreateWorkspaceMembers>>(
&self,
workspace_uuid: uuid::Uuid,
workspace_uuid: Uuid,
members: T,
) -> Result<(), AppError> {
let members = members.into();
@ -392,23 +395,42 @@ impl Client {
}
#[instrument(level = "debug", skip_all, err)]
pub async fn remove_workspace_members(
pub async fn update_workspace_member(
&self,
workspace_uuid: uuid::Uuid,
member_emails: Vec<String>,
workspace_uuid: Uuid,
changeset: WorkspaceMemberChangeset,
) -> Result<(), AppError> {
let url = format!("{}/api/workspace/{}/member", self.base_url, workspace_uuid);
let req = WorkspaceMembers(member_emails);
let resp = self
.http_client_with_auth(Method::DELETE, &url)
.http_client_with_auth(Method::PUT, &url)
.await?
.json(&req)
.json(&changeset)
.send()
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()?;
Ok(())
}
#[instrument(level = "debug", skip_all, err)]
pub async fn remove_workspace_members(
&self,
workspace_uuid: Uuid,
member_emails: Vec<String>,
) -> Result<(), AppError> {
let url = format!("{}/api/workspace/{}/member", self.base_url, workspace_uuid);
let payload = WorkspaceMembers::from(member_emails);
let resp = self
.http_client_with_auth(Method::DELETE, &url)
.await?
.json(&payload)
.send()
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()?;
Ok(())
}
// pub async fn update_workspace_member(&self, workspace_uuid: Uuid, member)
#[instrument(skip_all, err)]
pub async fn sign_in_password(&self, email: &str, password: &str) -> Result<bool, AppError> {
let access_token_resp = self

View file

@ -24,6 +24,9 @@ pub enum DatabaseError {
#[error("Bucket error:{0}")]
BucketError(String),
#[error("Not enough permission:{0}")]
NotEnoughPermissions(String),
#[error(transparent)]
Internal(#[from] anyhow::Error),
}

View file

@ -182,26 +182,17 @@ impl AFWorkspaces {
}
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone)]
pub enum AFRole {
Owner,
Member,
Guest,
}
impl AFRole {
pub fn id(&self) -> i32 {
match self {
AFRole::Owner => 1,
AFRole::Member => 2,
AFRole::Guest => 3,
}
}
}
impl From<i32> for AFRole {
fn from(item: i32) -> Self {
match item {
fn from(value: i32) -> Self {
// Can't modify the value of the enum
match value {
1 => AFRole::Owner,
2 => AFRole::Member,
3 => AFRole::Guest,
@ -210,6 +201,16 @@ impl From<i32> for AFRole {
}
}
impl From<AFRole> for i32 {
fn from(role: AFRole) -> Self {
// Can't modify the value of the enum
match role {
AFRole::Owner => 1,
AFRole::Member => 2,
AFRole::Guest => 3,
}
}
}
#[derive(FromRow, Serialize, Deserialize)]
pub struct AFWorkspaceMember {
pub email: String,

View file

@ -25,20 +25,29 @@ pub async fn select_all_workspaces_owned(
Ok(workspaces)
}
/// Checks whether a user, identified by a UUID, is an 'Owner' of a workspace, identified by its
/// workspace_id.
pub async fn select_user_is_workspace_owner(
pg_pool: &PgPool,
user_uuid: &Uuid,
workspace_uuid: &Uuid,
) -> Result<bool, DatabaseError> {
// 1. Identifies the user's UID in the 'af_user' table using the provided user UUID ($2).
// 2. Then, it checks the 'af_workspace_member' table to find a record that matches the provided workspace_id ($1) and the identified UID.
// 3. It joins with 'af_roles' to ensure that the role associated with the workspace member is 'Owner'.
let exists = sqlx::query_scalar!(
r#"
SELECT EXISTS(
SELECT 1 FROM public.af_workspace
WHERE workspace_id = $1 AND owner_uid = (
SELECT uid FROM public.af_user WHERE uuid = $2
)
)
"#,
SELECT EXISTS(
SELECT 1
FROM public.af_workspace_member
JOIN af_roles ON af_workspace_member.role_id = af_roles.id
WHERE workspace_id = $1
AND af_workspace_member.uid = (
SELECT uid FROM public.af_user WHERE uuid = $2
)
AND af_roles.name = 'Owner'
) AS "exists";
"#,
workspace_uuid,
user_uuid
)
@ -54,6 +63,7 @@ pub async fn insert_workspace_member(
member_email: String,
role: AFRole,
) -> Result<(), DatabaseError> {
let role_id: i32 = role.into();
sqlx::query!(
r#"
INSERT INTO public.af_workspace_member (workspace_id, uid, role_id)
@ -65,7 +75,7 @@ pub async fn insert_workspace_member(
"#,
workspace_id,
member_email,
role.id()
role_id
)
.execute(txn.deref_mut())
.await?;
@ -73,20 +83,29 @@ pub async fn insert_workspace_member(
Ok(())
}
pub async fn delete_workspace_members(
pub async fn upsert_workspace_member(
pool: &PgPool,
workspace_id: &uuid::Uuid,
member_emails: &[String],
) -> Result<(), DatabaseError> {
workspace_id: &Uuid,
email: &str,
role: Option<AFRole>,
) -> Result<(), sqlx::Error> {
if role.is_none() {
return Ok(());
}
let role_id: Option<i32> = role.map(|role| role.into());
sqlx::query!(
r#"
DELETE FROM public.af_workspace_member
WHERE workspace_id = $1 AND uid IN (
SELECT uid FROM public.af_user WHERE email = ANY($2::text[])
UPDATE af_workspace_member
SET
role_id = COALESCE($1, role_id)
WHERE workspace_id = $2 AND uid = (
SELECT uid FROM af_user WHERE email = $3
)
"#,
role_id,
workspace_id,
&member_emails
email
)
.execute(pool)
.await?;
@ -94,6 +113,59 @@ pub async fn delete_workspace_members(
Ok(())
}
pub async fn delete_workspace_members(
_user_uuid: &Uuid,
txn: &mut Transaction<'_, sqlx::Postgres>,
workspace_id: &Uuid,
member_email: String,
) -> Result<(), DatabaseError> {
let is_owner = sqlx::query_scalar!(
r#"
SELECT EXISTS (
SELECT 1
FROM public.af_workspace
WHERE
workspace_id = $1
AND owner_uid = (
SELECT uid FROM public.af_user WHERE email = $2
)
) AS "is_owner";
"#,
workspace_id,
member_email
)
.fetch_one(txn.deref_mut())
.await?
.unwrap_or(false);
if is_owner {
return Err(DatabaseError::NotEnoughPermissions(
"Owner cannot be deleted".to_string(),
));
}
sqlx::query!(
r#"
DELETE FROM public.af_workspace_member
WHERE
workspace_id = $1
AND uid = (
SELECT uid FROM public.af_user WHERE email = $2
)
-- Ensure the user to be deleted is not the original owner
AND uid <> (
SELECT owner_uid FROM public.af_workspace WHERE workspace_id = $1
);
"#,
workspace_id,
member_email,
)
.execute(txn.deref_mut())
.await?;
Ok(())
}
pub async fn select_workspace_members(
pg_pool: &PgPool,
workspace_id: &uuid::Uuid,

View file

@ -57,6 +57,9 @@ impl From<DatabaseError> for AppError {
match &value {
DatabaseError::RecordNotFound => AppError::new(ErrorCode::RecordNotFound, value),
DatabaseError::UnexpectedData(_) => AppError::new(ErrorCode::InvalidRequestParams, value),
DatabaseError::NotEnoughPermissions(msg) => {
AppError::new(ErrorCode::NotEnoughPermissions, msg.clone())
},
DatabaseError::StorageSpaceNotEnough => {
AppError::new(ErrorCode::StorageSpaceNotEnough, value)
},

View file

@ -1,44 +1,7 @@
// Data Transfer Objects (DTO)
use database_entity::AFRole;
use gotrue_entity::AccessTokenResponse;
#[derive(serde::Deserialize, serde::Serialize)]
pub struct WorkspaceMembers(pub Vec<String>);
#[derive(serde::Deserialize, serde::Serialize)]
pub struct CreateWorkspaceMembers(pub Vec<CreateWorkspaceMember>);
impl From<Vec<CreateWorkspaceMember>> for CreateWorkspaceMembers {
fn from(value: Vec<CreateWorkspaceMember>) -> Self {
Self(value)
}
}
#[derive(serde_repr::Deserialize_repr, serde_repr::Serialize_repr)]
#[repr(u8)]
pub enum WorkspacePermission {
Owner = 0,
Member = 1,
Guest = 2,
}
impl From<WorkspacePermission> for AFRole {
fn from(value: WorkspacePermission) -> Self {
match value {
WorkspacePermission::Owner => AFRole::Owner,
WorkspacePermission::Member => AFRole::Member,
WorkspacePermission::Guest => AFRole::Guest,
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct CreateWorkspaceMember {
pub email: String,
pub permission: WorkspacePermission,
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct SignInParams {
pub email: String,

View file

@ -0,0 +1,2 @@
pub mod auth_dto;
pub mod workspace_dto;

View file

@ -0,0 +1,60 @@
use database_entity::AFRole;
use std::ops::Deref;
#[derive(serde::Deserialize, serde::Serialize)]
pub struct WorkspaceMembers(pub Vec<WorkspaceMember>);
#[derive(serde::Deserialize, serde::Serialize)]
pub struct WorkspaceMember(pub String);
impl Deref for WorkspaceMember {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Vec<String>> for WorkspaceMembers {
fn from(value: Vec<String>) -> Self {
Self(value.into_iter().map(WorkspaceMember).collect())
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct CreateWorkspaceMembers(pub Vec<CreateWorkspaceMember>);
impl From<Vec<CreateWorkspaceMember>> for CreateWorkspaceMembers {
fn from(value: Vec<CreateWorkspaceMember>) -> Self {
Self(value)
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct CreateWorkspaceMember {
pub email: String,
pub role: AFRole,
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct WorkspaceMemberChangeset {
pub email: String,
pub role: Option<AFRole>,
pub name: Option<String>,
}
impl WorkspaceMemberChangeset {
pub fn new(email: String) -> Self {
Self {
email,
role: None,
name: None,
}
}
pub fn with_role(mut self, role: AFRole) -> Self {
self.role = Some(role);
self
}
pub fn with_name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
}

View file

@ -1,7 +1,7 @@
pub mod app_error;
pub mod data;
pub mod dto;
pub mod error_code;
#[cfg(feature = "cloud")]
mod data_actix;
pub mod dto;

View file

@ -11,7 +11,7 @@ use crate::domain::{UserEmail, UserName, UserPassword};
use crate::state::AppState;
use database_entity::AFUserProfileView;
use shared_entity::data::{AppResponse, JsonAppResponse};
use shared_entity::dto::{SignInTokenResponse, UpdateUsernameParams};
use shared_entity::dto::auth_dto::{SignInTokenResponse, UpdateUsernameParams};
use crate::component::auth::jwt::{Authorization, UserUuid};
use actix_web::web::{Data, Json};

View file

@ -8,7 +8,7 @@ use actix_web::Result;
use actix_web::{web, Scope};
use database_entity::{AFWorkspaceMember, AFWorkspaces};
use shared_entity::data::{AppResponse, JsonAppResponse};
use shared_entity::dto::{CreateWorkspaceMembers, WorkspaceMembers};
use shared_entity::dto::workspace_dto::*;
use sqlx::types::uuid;
use std::collections::HashMap;
use std::sync::Arc;
@ -21,7 +21,6 @@ pub const WORKSPACE_ID_PATH: &str = "workspace_id";
const SCOPE_PATH: &str = "/api/workspace";
const WORKSPACE_LIST_PATH: &str = "list";
const WORKSPACE_MEMBER_PATH: &str = "{workspace_id}/member";
const WORKSPACE_MEMBER_PERMISSION_PATH: &str = "{workspace_id}/member/permission";
pub fn workspace_scope() -> Scope {
web::scope(SCOPE_PATH)
@ -30,12 +29,9 @@ pub fn workspace_scope() -> Scope {
web::resource(WORKSPACE_MEMBER_PATH)
.route(web::get().to(list_workspace_members_handler))
.route(web::post().to(add_workspace_members_handler))
.route(web::put().to(update_workspace_member_handler))
.route(web::delete().to(remove_workspace_member_handler)),
)
.service(
web::resource(WORKSPACE_MEMBER_PERMISSION_PATH)
.route(web::post().to(update_workspace_member_permission_handler)),
)
}
pub fn workspace_scope_access_control(
@ -53,11 +49,6 @@ pub fn workspace_scope_access_control(
Arc::new(WorkspaceOwnerAccessControl),
);
access_control.insert(
format!("{}/{}", SCOPE_PATH, WORKSPACE_MEMBER_PERMISSION_PATH),
Arc::new(WorkspaceOwnerAccessControl),
);
access_control
}
@ -106,18 +97,30 @@ async fn remove_workspace_member_handler(
state: Data<AppState>,
workspace_id: web::Path<Uuid>,
) -> Result<JsonAppResponse<()>> {
let members = payload.into_inner();
workspace::ops::remove_workspace_members(&state.pg_pool, &user_uuid, &workspace_id, &members.0)
.await?;
let member_emails = payload
.into_inner()
.0
.into_iter()
.map(|member| member.0)
.collect();
workspace::ops::remove_workspace_members(
&user_uuid,
&state.pg_pool,
workspace_id.into_inner(),
member_emails,
)
.await?;
Ok(AppResponse::Ok().into())
}
#[instrument(skip_all, err)]
async fn update_workspace_member_permission_handler(
_user_uuid: UserUuid,
_req: Json<CreateWorkspaceMembers>,
_state: Data<AppState>,
_workspace_id: web::Path<Uuid>,
async fn update_workspace_member_handler(
payload: Json<WorkspaceMemberChangeset>,
state: Data<AppState>,
workspace_id: web::Path<Uuid>,
) -> Result<JsonAppResponse<()>> {
todo!()
let workspace_id = workspace_id.into_inner();
let changeset = payload.into_inner();
workspace::ops::update_workspace_member(&state.pg_pool, &workspace_id, changeset).await?;
Ok(AppResponse::Ok().into())
}

View file

@ -1,10 +1,11 @@
use crate::component::auth::jwt::UserUuid;
use anyhow::Context;
use database::workspace::{
delete_workspace_members, insert_workspace_member, select_all_workspaces_owned,
select_user_is_workspace_owner, select_workspace_members,
select_user_is_workspace_owner, select_workspace_members, upsert_workspace_member,
};
use database_entity::{AFWorkspaceMember, AFWorkspaces};
use shared_entity::dto::CreateWorkspaceMember;
use shared_entity::dto::workspace_dto::{CreateWorkspaceMember, WorkspaceMemberChangeset};
use shared_entity::{app_error::AppError, error_code::ErrorCode};
use sqlx::{types::uuid, PgPool};
@ -27,13 +28,7 @@ pub async fn add_workspace_members(
.await
.context("Begin transaction to insert workspace members")?;
for member in members {
insert_workspace_member(
&mut txn,
workspace_id,
member.email,
member.permission.into(),
)
.await?;
insert_workspace_member(&mut txn, workspace_id, member.email, member.role).await?;
}
txn
@ -44,12 +39,25 @@ pub async fn add_workspace_members(
}
pub async fn remove_workspace_members(
user_uuid: &UserUuid,
pg_pool: &PgPool,
_user_uuid: &uuid::Uuid,
workspace_id: &uuid::Uuid,
member_emails: &[String],
workspace_id: uuid::Uuid,
member_emails: Vec<String>,
) -> Result<(), AppError> {
Ok(delete_workspace_members(pg_pool, workspace_id, member_emails).await?)
let mut txn = pg_pool
.begin()
.await
.context("Begin transaction to delete workspace members")?;
for email in member_emails {
delete_workspace_members(user_uuid, &mut txn, &workspace_id, email).await?;
}
txn
.commit()
.await
.context("Commit transaction to delete workspace members")?;
Ok(())
}
pub async fn get_workspace_members(
@ -61,13 +69,13 @@ pub async fn get_workspace_members(
}
#[allow(dead_code)]
pub async fn update_workspace_member_permission(
_pg_pool: &PgPool,
_user_uuid: &uuid::Uuid,
_workspace_id: &uuid::Uuid,
_member_emails: &[String],
pub async fn update_workspace_member(
pg_pool: &PgPool,
workspace_id: &uuid::Uuid,
changeset: WorkspaceMemberChangeset,
) -> Result<(), AppError> {
todo!()
upsert_workspace_member(pg_pool, workspace_id, &changeset.email, changeset.role).await?;
Ok(())
}
pub async fn require_user_is_workspace_owner(

View file

@ -4,7 +4,7 @@ use crate::middleware::permission_mw::WorkspaceAccessControlService;
use async_trait::async_trait;
use shared_entity::app_error::AppError;
use sqlx::PgPool;
use tracing::trace;
use tracing::{debug, trace};
use uuid::Uuid;
#[derive(Clone)]
@ -19,7 +19,9 @@ impl WorkspaceAccessControlService for WorkspaceOwnerAccessControl {
pg_pool: &PgPool,
) -> Result<(), AppError> {
let result = require_user_is_workspace_owner(pg_pool, &user_uuid, &workspace_id).await;
trace!("Workspace owner access control: {:?}", result);
if let Err(err) = result.as_ref() {
debug!("Workspace access control: {:?}", err);
}
result
}
}

View file

@ -1,4 +1,4 @@
use shared_entity::dto::UserUpdateParams;
use shared_entity::dto::auth_dto::UserUpdateParams;
use shared_entity::error_code::ErrorCode;
use crate::localhost_client;

View file

@ -1,25 +1,23 @@
use shared_entity::dto::{CreateWorkspaceMember, WorkspacePermission};
use database_entity::AFRole;
use shared_entity::dto::workspace_dto::{CreateWorkspaceMember, WorkspaceMemberChangeset};
use shared_entity::error_code::ErrorCode;
use crate::user::utils::generate_unique_registered_user_client;
#[tokio::test]
async fn add_workspace_members_not_enough_permission() {
let (c1, _user1) = generate_unique_registered_user_client().await;
let (c1, user1) = generate_unique_registered_user_client().await;
let (c2, _user2) = generate_unique_registered_user_client().await;
let user2_workspace = c2.workspaces().await.unwrap();
let user2_workspace_id = user2_workspace.first().unwrap().workspace_id;
let workspace_id_2 = c2.workspaces().await.unwrap().first().unwrap().workspace_id;
// attempt to add user2 to user1's workspace
// using user1's client
let email = c1.token().read().as_ref().unwrap().user.email.to_owned();
let err = c1
.add_workspace_members(
user2_workspace_id,
workspace_id_2,
vec![CreateWorkspaceMember {
email,
permission: WorkspacePermission::Member,
email: user1.email,
role: AFRole::Member,
}],
)
.await
@ -41,7 +39,7 @@ async fn add_workspace_members_then_delete() {
c1_workspace_id,
vec![CreateWorkspaceMember {
email,
permission: WorkspacePermission::Member,
role: AFRole::Member,
}],
)
.await
@ -72,3 +70,152 @@ async fn add_workspace_members_then_delete() {
.any(|w| w.email == *c2_email));
}
}
#[tokio::test]
async fn workspace_member_add_new_member() {
let (c1, _user1) = generate_unique_registered_user_client().await;
let (c2, user2) = generate_unique_registered_user_client().await;
let (_c3, user3) = generate_unique_registered_user_client().await;
let workspace_id = c1.workspaces().await.unwrap().first().unwrap().workspace_id;
c1.add_workspace_members(
workspace_id,
vec![CreateWorkspaceMember {
email: user2.email,
role: AFRole::Member,
}],
)
.await
.unwrap();
let err = c2
.add_workspace_members(
workspace_id,
vec![CreateWorkspaceMember {
email: user3.email,
role: AFRole::Member,
}],
)
.await
.unwrap_err();
assert_eq!(err.code, ErrorCode::NotEnoughPermissions);
}
#[tokio::test]
async fn workspace_owner_add_new_owner() {
let (c1, user1) = generate_unique_registered_user_client().await;
let (_c2, user2) = generate_unique_registered_user_client().await;
let workspace_id = c1.workspaces().await.unwrap().first().unwrap().workspace_id;
c1.add_workspace_members(
workspace_id,
vec![CreateWorkspaceMember {
email: user2.email.clone(),
role: AFRole::Owner,
}],
)
.await
.unwrap();
let members = c1.get_workspace_members(workspace_id).await.unwrap();
assert_eq!(members[0].email, user1.email);
assert_eq!(members[0].role, AFRole::Owner);
assert_eq!(members[1].email, user2.email);
assert_eq!(members[1].role, AFRole::Owner);
}
#[tokio::test]
async fn workspace_second_owner_add_new_member() {
let (c1, user1) = generate_unique_registered_user_client().await;
let (c2, user2) = generate_unique_registered_user_client().await;
let (_c3, user3) = generate_unique_registered_user_client().await;
let workspace_id = c1.workspaces().await.unwrap().first().unwrap().workspace_id;
c1.add_workspace_members(
workspace_id,
vec![CreateWorkspaceMember {
email: user2.email.clone(),
role: AFRole::Owner,
}],
)
.await
.unwrap();
c2.add_workspace_members(
workspace_id,
vec![CreateWorkspaceMember {
email: user3.email.clone(),
role: AFRole::Member,
}],
)
.await
.unwrap();
let members = c1.get_workspace_members(workspace_id).await.unwrap();
assert_eq!(members[0].email, user1.email);
assert_eq!(members[0].role, AFRole::Owner);
assert_eq!(members[1].email, user2.email);
assert_eq!(members[1].role, AFRole::Owner);
assert_eq!(members[2].email, user3.email);
assert_eq!(members[2].role, AFRole::Member);
}
#[tokio::test]
async fn workspace_second_owner_can_not_delete_origin_owner() {
let (c1, user1) = generate_unique_registered_user_client().await;
let (c2, user2) = generate_unique_registered_user_client().await;
let workspace_id = c1.workspaces().await.unwrap().first().unwrap().workspace_id;
c1.add_workspace_members(
workspace_id,
vec![CreateWorkspaceMember {
email: user2.email.clone(),
role: AFRole::Owner,
}],
)
.await
.unwrap();
let err = c2
.remove_workspace_members(workspace_id, [user1.email].to_vec())
.await
.unwrap_err();
assert_eq!(err.code, ErrorCode::NotEnoughPermissions);
}
#[tokio::test]
async fn workspace_owner_update_member_role() {
let (c1, _user1) = generate_unique_registered_user_client().await;
let (_c2, user2) = generate_unique_registered_user_client().await;
let workspace_id = c1.workspaces().await.unwrap().first().unwrap().workspace_id;
c1.add_workspace_members(
workspace_id,
vec![CreateWorkspaceMember {
email: user2.email.clone(),
role: AFRole::Member,
}],
)
.await
.unwrap();
let members = c1.get_workspace_members(workspace_id).await.unwrap();
assert_eq!(members[1].email, user2.email);
assert_eq!(members[1].role, AFRole::Member);
// Update user2's role to Owner
c1.update_workspace_member(
workspace_id,
WorkspaceMemberChangeset::new(user2.email.clone()).with_role(AFRole::Owner),
)
.await
.unwrap();
let members = c1.get_workspace_members(workspace_id).await.unwrap();
assert_eq!(members[1].role, AFRole::Owner);
}