feat: add endpoints to allow workspace owner to approve web page view request

This commit is contained in:
khorshuheng 2024-09-26 09:03:39 +08:00
parent b023e2f511
commit 813fa29253
23 changed files with 753 additions and 1 deletions

View file

@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "\n DELETE FROM af_access_request\n WHERE request_id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": []
},
"hash": "4476f271f4ea8c83428b4178c43ee2894e380a7c3ae3cbc782f438fabc45de8b"
}

View file

@ -0,0 +1,25 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO af_access_request (\n workspace_id,\n view_id,\n uid,\n status\n )\n VALUES ($1, $2, $3, $4)\n RETURNING request_id\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "request_id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid",
"Uuid",
"Int8",
"Int4"
]
},
"nullable": [
false
]
},
"hash": "598e731078fc6417039cc16772eb5bc6c74d24c1a8018a981d2175a483dc699c"
}

View file

@ -0,0 +1,52 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT\n request_id,\n view_id,\n (\n workspace_id,\n af_workspace.database_storage_id,\n af_workspace.owner_uid,\n owner_profile.name,\n af_workspace.created_at,\n af_workspace.workspace_type,\n af_workspace.deleted_at,\n af_workspace.workspace_name,\n af_workspace.icon\n ) AS \"workspace!: AFWorkspaceRow\",\n (\n af_user.uuid,\n af_user.email,\n af_user.name,\n af_user.metadata ->> 'avatar'\n ) AS \"requester!: AFAccessRequesterColumn\",\n status AS \"status: AFAccessRequestStatusColumn\",\n af_access_request.created_at AS created_at\n FROM af_access_request\n JOIN af_user USING (uid)\n JOIN af_workspace USING (workspace_id)\n JOIN af_user AS owner_profile ON af_workspace.owner_uid = owner_profile.uid\n WHERE request_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "request_id",
"type_info": "Uuid"
},
{
"ordinal": 1,
"name": "view_id",
"type_info": "Uuid"
},
{
"ordinal": 2,
"name": "workspace!: AFWorkspaceRow",
"type_info": "Record"
},
{
"ordinal": 3,
"name": "requester!: AFAccessRequesterColumn",
"type_info": "Record"
},
{
"ordinal": 4,
"name": "status: AFAccessRequestStatusColumn",
"type_info": "Int4"
},
{
"ordinal": 5,
"name": "created_at",
"type_info": "Timestamptz"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false,
false,
null,
null,
false,
false
]
},
"hash": "6317de690a65f0cb63e2f9d4889fc929f05df7ccfc0b7cb955787d0f88f91c45"
}

View file

@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE af_access_request\n SET status = $2\n WHERE request_id = $1\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Int4"
]
},
"nullable": []
},
"hash": "95b1b405028c45c074121110d046f42f8229f150c2384671802ee7c1ef9e376d"
}

View file

@ -11,6 +11,7 @@ use appflowy_ai_client::error::AIError;
use reqwest::StatusCode;
use serde::Serialize;
use thiserror::Error;
use uuid::Uuid;
#[derive(Debug, Error, Default)]
pub enum AppError {
@ -145,6 +146,12 @@ pub enum AppError {
#[error("{0}")]
NotInviteeOfWorkspaceInvitation(String),
#[error("{0}")]
MissingView(String),
#[error("There is existing access request for workspace {workspace_id} and view {view_id}")]
AccessRequestAlreadyExists { workspace_id: Uuid, view_id: Uuid },
}
impl AppError {
@ -212,6 +219,8 @@ impl AppError {
AppError::InvalidPublishedOutline(_) => ErrorCode::InvalidPublishedOutline,
AppError::InvalidFolderView(_) => ErrorCode::InvalidFolderView,
AppError::NotInviteeOfWorkspaceInvitation(_) => ErrorCode::NotInviteeOfWorkspaceInvitation,
AppError::MissingView(_) => ErrorCode::MissingView,
AppError::AccessRequestAlreadyExists { .. } => ErrorCode::AccessRequestAlreadyExists,
}
}
}
@ -339,6 +348,8 @@ pub enum ErrorCode {
InvalidPublishedOutline = 1039,
InvalidFolderView = 1040,
NotInviteeOfWorkspaceInvitation = 1041,
MissingView = 1042,
AccessRequestAlreadyExists = 1043,
}
impl ErrorCode {

View file

@ -0,0 +1,76 @@
use client_api_entity::{
access_request_dto::AccessRequest, AccessRequestMinimal, ApproveAccessRequestParams,
CreateAccessRequestParams,
};
use reqwest::Method;
use shared_entity::response::{AppResponse, AppResponseError};
use uuid::Uuid;
use crate::Client;
impl Client {
pub async fn get_access_request(
&self,
access_request_id: Uuid,
) -> Result<AccessRequest, AppResponseError> {
let url = format!("{}/api/access-request/{}", self.base_url, access_request_id);
let resp = self
.http_client_with_auth(Method::GET, &url)
.await?
.send()
.await?;
AppResponse::<AccessRequest>::from_response(resp)
.await?
.into_data()
}
pub async fn create_access_request(
&self,
data: CreateAccessRequestParams,
) -> Result<AccessRequestMinimal, AppResponseError> {
let url = format!("{}/api/access-request", self.base_url);
let resp = self
.http_client_with_auth(Method::POST, &url)
.await?
.json(&data)
.send()
.await?;
AppResponse::<AccessRequestMinimal>::from_response(resp)
.await?
.into_data()
}
pub async fn approve_access_request(
&self,
access_request_id: Uuid,
) -> Result<(), AppResponseError> {
let url = format!(
"{}/api/access-request/{}/approve",
self.base_url, access_request_id
);
let resp = self
.http_client_with_auth(Method::POST, &url)
.await?
.json(&ApproveAccessRequestParams { is_approved: true })
.send()
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()
}
pub async fn reject_access_request(
&self,
access_request_id: Uuid,
) -> Result<(), AppResponseError> {
let url = format!(
"{}/api/access-request/{}/approve",
self.base_url, access_request_id
);
let resp = self
.http_client_with_auth(Method::POST, &url)
.await?
.json(&ApproveAccessRequestParams { is_approved: false })
.send()
.await?;
AppResponse::<()>::from_response(resp).await?.into_error()
}
}

View file

@ -2,6 +2,7 @@ mod http;
mod http_ai;
mod http_billing;
mod http_access_request;
mod http_blob;
mod http_collab;
mod http_history;

View file

@ -1238,6 +1238,51 @@ pub struct AvatarImageSource {
pub file_id: String,
}
#[derive(Serialize_repr, Deserialize_repr, PartialEq, Debug, Copy, Clone)]
#[repr(i32)]
pub enum AccessRequestStatus {
Pending = 0,
Approved = 1,
Rejected = 2,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AccessRequestWithViewId {
pub request_id: Uuid,
pub workspace: AFWorkspace,
pub requester: AccessRequesterInfo,
pub view_id: Uuid,
pub status: AccessRequestStatus,
pub created_at: DateTime<Utc>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AccessRequesterInfo {
pub uuid: Uuid,
pub email: String,
pub name: String,
pub avatar_url: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AccessRequestMinimal {
pub request_id: Uuid,
pub workspace_id: Uuid,
pub requester_id: Uuid,
pub view_id: Uuid,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct CreateAccessRequestParams {
pub workspace_id: Uuid,
pub view_id: Uuid,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ApproveAccessRequestParams {
pub is_approved: bool,
}
#[cfg(test)]
mod test {
use crate::dto::{CollabParams, CollabParamsV0};

View file

@ -0,0 +1,127 @@
use crate::pg_row::{
AFAccessRequestStatusColumn, AFAccessRequestWithViewIdColumn, AFAccessRequesterColumn,
AFWorkspaceRow,
};
use app_error::AppError;
use database_entity::dto::AccessRequestWithViewId;
use sqlx::{Executor, Postgres};
use uuid::Uuid;
pub async fn insert_new_access_request<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
workspace_id: Uuid,
view_id: Uuid,
uid: i64,
) -> Result<Uuid, AppError> {
let request_id_result = sqlx::query_scalar!(
r#"
INSERT INTO af_access_request (
workspace_id,
view_id,
uid,
status
)
VALUES ($1, $2, $3, $4)
RETURNING request_id
"#,
workspace_id,
view_id,
uid,
AFAccessRequestStatusColumn::Pending as _,
)
.fetch_one(executor)
.await;
match request_id_result {
Err(e)
if e
.as_database_error()
.map_or(false, |e| e.constraint().is_some()) =>
{
Err(AppError::AccessRequestAlreadyExists {
workspace_id,
view_id,
})
},
Err(e) => Err(e.into()),
Ok(request_id) => Ok(request_id),
}
}
pub async fn select_access_request_by_request_id<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
request_id: Uuid,
) -> Result<AccessRequestWithViewId, AppError> {
let access_request = sqlx::query_as!(
AFAccessRequestWithViewIdColumn,
r#"
SELECT
request_id,
view_id,
(
workspace_id,
af_workspace.database_storage_id,
af_workspace.owner_uid,
owner_profile.name,
af_workspace.created_at,
af_workspace.workspace_type,
af_workspace.deleted_at,
af_workspace.workspace_name,
af_workspace.icon
) AS "workspace!: AFWorkspaceRow",
(
af_user.uuid,
af_user.email,
af_user.name,
af_user.metadata ->> 'avatar'
) AS "requester!: AFAccessRequesterColumn",
status AS "status: AFAccessRequestStatusColumn",
af_access_request.created_at AS created_at
FROM af_access_request
JOIN af_user USING (uid)
JOIN af_workspace USING (workspace_id)
JOIN af_user AS owner_profile ON af_workspace.owner_uid = owner_profile.uid
WHERE request_id = $1
"#,
request_id,
)
.fetch_one(executor)
.await?;
let access_request: AccessRequestWithViewId = access_request.try_into()?;
Ok(access_request)
}
pub async fn update_access_request_status<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
request_id: Uuid,
status: AFAccessRequestStatusColumn,
) -> Result<(), AppError> {
sqlx::query!(
r#"
UPDATE af_access_request
SET status = $2
WHERE request_id = $1
"#,
request_id,
status as _,
)
.execute(executor)
.await?;
Ok(())
}
pub async fn delete_access_request<'a, E: Executor<'a, Database = Postgres>>(
executor: E,
request_id: Uuid,
) -> Result<(), AppError> {
sqlx::query!(
r#"
DELETE FROM af_access_request
WHERE request_id = $1
"#,
request_id,
)
.execute(executor)
.await?;
Ok(())
}

View file

@ -1,3 +1,4 @@
pub mod access_request;
pub mod chat;
pub mod collab;
pub mod file;

View file

@ -4,6 +4,7 @@ use chrono::{DateTime, Utc};
use database_entity::dto::{
AFAccessLevel, AFRole, AFUserProfile, AFWebUser, AFWorkspace, AFWorkspaceInvitationStatus,
AccessRequestMinimal, AccessRequestStatus, AccessRequestWithViewId, AccessRequesterInfo,
AccountLink, GlobalComment, Reaction, Template, TemplateCategory, TemplateCategoryMinimal,
TemplateCategoryType, TemplateCreator, TemplateCreatorMinimal, TemplateGroup, TemplateMinimal,
};
@ -12,7 +13,7 @@ use sqlx::FromRow;
use uuid::Uuid;
/// Represent the row of the af_workspace table
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
#[derive(Debug, Clone, FromRow, Serialize, Deserialize, sqlx::Type)]
pub struct AFWorkspaceRow {
pub workspace_id: Uuid,
pub database_storage_id: Option<Uuid>,
@ -502,3 +503,84 @@ impl From<AFTemplateGroupRow> for TemplateGroup {
}
}
}
#[derive(sqlx::Type, Serialize, Deserialize, Debug)]
#[repr(i32)]
pub enum AFAccessRequestStatusColumn {
Pending = 0,
Approved = 1,
Rejected = 2,
}
impl From<AFAccessRequestStatusColumn> for AccessRequestStatus {
fn from(value: AFAccessRequestStatusColumn) -> Self {
match value {
AFAccessRequestStatusColumn::Pending => AccessRequestStatus::Pending,
AFAccessRequestStatusColumn::Approved => AccessRequestStatus::Approved,
AFAccessRequestStatusColumn::Rejected => AccessRequestStatus::Rejected,
}
}
}
#[derive(sqlx::Type, Serialize, Debug)]
pub struct AFAccessRequesterColumn {
pub uuid: Uuid,
pub name: String,
pub email: String,
pub avatar_url: Option<String>,
}
impl From<AFAccessRequesterColumn> for AccessRequesterInfo {
fn from(value: AFAccessRequesterColumn) -> Self {
Self {
uuid: value.uuid,
name: value.name,
email: value.email,
avatar_url: value.avatar_url,
}
}
}
#[derive(sqlx::Type, Serialize, Debug)]
pub struct AFAccessRequestMinimalColumn {
pub request_id: Uuid,
pub workspace_id: Uuid,
pub requester_id: Uuid,
pub view_id: Uuid,
}
impl From<AFAccessRequestMinimalColumn> for AccessRequestMinimal {
fn from(value: AFAccessRequestMinimalColumn) -> Self {
Self {
request_id: value.request_id,
workspace_id: value.workspace_id,
requester_id: value.requester_id,
view_id: value.view_id,
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AFAccessRequestWithViewIdColumn {
pub request_id: Uuid,
pub workspace: AFWorkspaceRow,
pub requester: AccessRequesterInfo,
pub view_id: Uuid,
pub status: AFAccessRequestStatusColumn,
pub created_at: DateTime<Utc>,
}
impl TryFrom<AFAccessRequestWithViewIdColumn> for AccessRequestWithViewId {
type Error = anyhow::Error;
fn try_from(value: AFAccessRequestWithViewIdColumn) -> Result<Self, Self::Error> {
Ok(Self {
request_id: value.request_id,
workspace: value.workspace.try_into()?,
requester: value.requester,
view_id: value.view_id,
status: value.status.into(),
created_at: value.created_at,
})
}
}

View file

@ -0,0 +1,23 @@
use database_entity::dto::{AFWorkspace, AccessRequestStatus, AccessRequesterInfo};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::workspace_dto::{ViewIcon, ViewLayout};
#[derive(Serialize, Deserialize, Debug)]
pub struct AccessRequestView {
pub view_id: String,
pub name: String,
pub icon: Option<ViewIcon>,
pub layout: ViewLayout,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct AccessRequest {
pub request_id: Uuid,
pub workspace: AFWorkspace,
pub requester: AccessRequesterInfo,
pub view: AccessRequestView,
pub status: AccessRequestStatus,
pub created_at: chrono::DateTime<chrono::Utc>,
}

View file

@ -1,3 +1,4 @@
pub mod access_request_dto;
pub mod ai_dto;
pub mod auth_dto;
pub mod billing_dto;

View file

@ -158,6 +158,14 @@ pub struct FolderView {
pub children: Vec<FolderView>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct FolderViewMinimal {
pub view_id: String,
pub name: String,
pub icon: Option<ViewIcon>,
pub layout: ViewLayout,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct SectionItems {
pub views: Vec<FolderView>,

View file

@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS af_access_request (
request_id UUID NOT NULL DEFAULT gen_random_uuid(),
uid BIGINT NOT NULL REFERENCES af_user(uid) ON DELETE CASCADE,
workspace_id UUID NOT NULL,
view_id UUID NOT NULL,
status INT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (uid, workspace_id, view_id),
PRIMARY KEY(request_id)
);
CREATE INDEX IF NOT EXISTS idx_workspace_id_on_af_access_request ON af_access_request(workspace_id);
CREATE INDEX IF NOT EXISTS idx_uid_on_af_access_request ON af_access_request(uid);

77
src/api/access_request.rs Normal file
View file

@ -0,0 +1,77 @@
use actix_web::{
web::{self, Data, Json},
Result, Scope,
};
use authentication::jwt::UserUuid;
use database_entity::dto::{
AccessRequestMinimal, ApproveAccessRequestParams, CreateAccessRequestParams,
};
use shared_entity::{
dto::access_request_dto::AccessRequest,
response::{AppResponse, JsonAppResponse},
};
use uuid::Uuid;
use crate::{
biz::access_request::ops::{
approve_or_reject_access_request, create_access_request, get_access_request,
},
state::AppState,
};
pub fn access_request_scope() -> Scope {
web::scope("/api/access-request")
.service(web::resource("").route(web::post().to(post_access_request_handler)))
.service(web::resource("/{request_id}").route(web::get().to(get_access_request_handler)))
.service(
web::resource("/{request_id}/approve")
.route(web::post().to(post_approve_access_request_handler)),
)
}
async fn get_access_request_handler(
_uuid: UserUuid,
access_request_id: web::Path<Uuid>,
state: Data<AppState>,
) -> Result<JsonAppResponse<AccessRequest>> {
let access_request_id = access_request_id.into_inner();
let access_request = get_access_request(
&state.pg_pool,
state.collab_access_control_storage.clone(),
access_request_id,
)
.await?;
Ok(Json(AppResponse::Ok().with_data(access_request)))
}
async fn post_access_request_handler(
uuid: UserUuid,
create_access_request_params: Json<CreateAccessRequestParams>,
state: Data<AppState>,
) -> Result<JsonAppResponse<AccessRequestMinimal>> {
let uid = state.user_cache.get_user_uid(&uuid).await?;
let workspace_id = create_access_request_params.workspace_id;
let view_id = create_access_request_params.view_id;
let request_id = create_access_request(&state.pg_pool, workspace_id, view_id, uid).await?;
let access_request = AccessRequestMinimal {
request_id,
workspace_id,
requester_id: *uuid,
view_id,
};
Ok(Json(AppResponse::Ok().with_data(access_request)))
}
async fn post_approve_access_request_handler(
uuid: UserUuid,
access_request_id: web::Path<Uuid>,
approve_access_request_params: Json<ApproveAccessRequestParams>,
state: Data<AppState>,
) -> Result<JsonAppResponse<()>> {
let uid = state.user_cache.get_user_uid(&uuid).await?;
let access_request_id = access_request_id.into_inner();
let is_approved = approve_access_request_params.is_approved;
approve_or_reject_access_request(&state.pg_pool, access_request_id, uid, *uuid, is_approved)
.await?;
Ok(Json(AppResponse::Ok()))
}

View file

@ -2,6 +2,7 @@ pub mod ai;
pub mod chat;
pub mod file_storage;
pub mod access_request;
pub mod history;
pub mod metrics;
pub mod search;

View file

@ -42,6 +42,7 @@ use snowflake::Snowflake;
use tonic_proto::history::history_client::HistoryClient;
use workspace_access::WorkspaceAccessControlImpl;
use crate::api::access_request::access_request_scope;
use crate::api::ai::ai_completion_scope;
use crate::api::chat::chat_scope;
use crate::api::file_storage::file_storage_scope;
@ -169,6 +170,7 @@ pub async fn run_actix_server(
.service(metrics_scope())
.service(search_scope())
.service(template_scope())
.service(access_request_scope())
.app_data(Data::new(state.metrics.registry.clone()))
.app_data(Data::new(state.metrics.request_metrics.clone()))
.app_data(Data::new(state.metrics.realtime_metrics.clone()))

View file

@ -0,0 +1 @@
pub mod ops;

View file

@ -0,0 +1,105 @@
use std::{ops::DerefMut, sync::Arc};
use anyhow::Context;
use app_error::AppError;
use appflowy_collaborate::collab::storage::CollabAccessControlStorage;
use database::{
access_request::{
insert_new_access_request, select_access_request_by_request_id, update_access_request_status,
},
collab::GetCollabOrigin,
pg_row::AFAccessRequestStatusColumn,
workspace::upsert_workspace_member_with_txn,
};
use database_entity::dto::AFRole;
use shared_entity::dto::access_request_dto::{AccessRequest, AccessRequestView};
use sqlx::PgPool;
use uuid::Uuid;
use crate::biz::collab::{
folder_view::{to_dto_view_icon, to_view_layout},
ops::get_latest_collab_folder,
};
pub async fn create_access_request(
pg_pool: &PgPool,
workspace_id: Uuid,
view_id: Uuid,
uid: i64,
) -> Result<Uuid, AppError> {
let request_id = insert_new_access_request(pg_pool, workspace_id, view_id, uid).await?;
Ok(request_id)
}
pub async fn get_access_request(
pg_pool: &PgPool,
collab_storage: Arc<CollabAccessControlStorage>,
access_request_id: Uuid,
) -> Result<AccessRequest, AppError> {
let access_request_with_view_id =
select_access_request_by_request_id(pg_pool, access_request_id).await?;
let folder = get_latest_collab_folder(
collab_storage,
GetCollabOrigin::Server,
&access_request_with_view_id
.workspace
.workspace_id
.to_string(),
)
.await?;
let view = folder.get_view(&access_request_with_view_id.view_id.to_string());
let access_request_view = view
.map(|v| AccessRequestView {
view_id: v.id.clone(),
name: v.name.clone(),
icon: v.icon.as_ref().map(|icon| to_dto_view_icon(icon.clone())),
layout: to_view_layout(&v.layout),
})
.ok_or(AppError::MissingView(format!(
"the view {} is missing",
access_request_with_view_id.view_id
)))?;
let access_request = AccessRequest {
request_id: access_request_with_view_id.request_id,
workspace: access_request_with_view_id.workspace,
requester: access_request_with_view_id.requester,
view: access_request_view,
status: access_request_with_view_id.status,
created_at: access_request_with_view_id.created_at,
};
Ok(access_request)
}
pub async fn approve_or_reject_access_request(
pg_pool: &PgPool,
request_id: Uuid,
uid: i64,
user_uuid: Uuid,
is_approved: bool,
) -> Result<(), AppError> {
let access_request = select_access_request_by_request_id(pg_pool, request_id).await?;
if access_request.workspace.owner_uid != uid {
return Err(AppError::NotEnoughPermissions {
user: user_uuid.to_string(),
action: "approve access request".to_string(),
});
}
let mut txn = pg_pool.begin().await.context("approving request")?;
let role = AFRole::Member;
upsert_workspace_member_with_txn(
&mut txn,
&access_request.workspace.workspace_id,
&access_request.requester.email,
role,
)
.await?;
let status = if is_approved {
AFAccessRequestStatusColumn::Approved
} else {
AFAccessRequestStatusColumn::Rejected
};
update_access_request_status(txn.deref_mut(), request_id, status).await?;
txn.commit().await.context("committing transaction")?;
Ok(())
}

View file

@ -1,3 +1,4 @@
pub mod access_request;
pub mod chat;
pub mod collab;
pub mod pg_listener;

View file

@ -0,0 +1,70 @@
use app_error::ErrorCode;
use client_api::entity::CreateAccessRequestParams;
use client_api_test::generate_unique_registered_user_client;
use shared_entity::dto::workspace_dto::ViewLayout;
use uuid::Uuid;
#[tokio::test]
async fn access_request_test() {
let (owner_client, _) = generate_unique_registered_user_client().await;
let workspaces = owner_client.get_workspaces().await.unwrap();
let workspace_id = workspaces[0].workspace_id;
let folder_view = owner_client
.get_workspace_folder(&workspace_id.to_string(), Some(2), None)
.await
.unwrap();
let view_id = folder_view
.children
.into_iter()
.find(|v| v.name == "General")
.unwrap()
.children
.iter()
.find(|v| v.name == "To-dos")
.unwrap()
.view_id
.clone();
let view_id = Uuid::parse_str(&view_id).unwrap();
let data = CreateAccessRequestParams {
workspace_id,
view_id,
};
let (requester_client, requester) = generate_unique_registered_user_client().await;
let access_request = requester_client
.create_access_request(data.clone())
.await
.unwrap();
let resp = requester_client.create_access_request(data).await;
assert!(resp.is_err());
assert_eq!(
resp.unwrap_err().code,
ErrorCode::AccessRequestAlreadyExists
);
let access_request_id = access_request.request_id;
let access_request_to_be_approved = owner_client
.get_access_request(access_request_id)
.await
.unwrap();
assert_eq!(
access_request_to_be_approved.requester.email,
requester.email
);
assert_eq!(
access_request_to_be_approved.view.view_id,
view_id.to_string()
);
assert_eq!(access_request_to_be_approved.view.layout, ViewLayout::Board);
assert_eq!(
access_request_to_be_approved.workspace.workspace_id,
workspace_id
);
owner_client
.approve_access_request(access_request_id)
.await
.unwrap();
let workspace_members = owner_client
.get_workspace_members(workspace_id.to_string())
.await
.unwrap();
assert!(workspace_members.iter().any(|m| m.email == requester.email));
}

View file

@ -1,3 +1,4 @@
mod access_request;
mod default_user_workspace;
mod edit_workspace;
mod invitation_crud;