618 lines
18 KiB
Rust
618 lines
18 KiB
Rust
use std::collections::HashMap;
|
|
use std::thread::sleep;
|
|
use std::time::Duration;
|
|
|
|
use app_error::ErrorCode;
|
|
use client_api::entity::{AFRole, GlobalComment, PublishCollabItem, PublishCollabMetadata};
|
|
use client_api_test::TestClient;
|
|
use client_api_test::{generate_unique_registered_user_client, localhost_client};
|
|
use itertools::Itertools;
|
|
|
|
#[tokio::test]
|
|
async fn test_set_publish_namespace_set() {
|
|
let (c, _user) = generate_unique_registered_user_client().await;
|
|
let workspace_id = get_first_workspace_string(&c).await;
|
|
|
|
{
|
|
// can get namespace before setting, which is random string
|
|
let _ = c
|
|
.get_workspace_publish_namespace(&workspace_id.to_string())
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
let namespace = uuid::Uuid::new_v4().to_string();
|
|
c.set_workspace_publish_namespace(&workspace_id.to_string(), &namespace)
|
|
.await
|
|
.unwrap();
|
|
|
|
{
|
|
// cannot set the same namespace
|
|
let err = c
|
|
.set_workspace_publish_namespace(&workspace_id.to_string(), &namespace)
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert_eq!(format!("{:?}", err.code), "PublishNamespaceAlreadyTaken");
|
|
}
|
|
{
|
|
// can replace the namespace
|
|
let namespace = uuid::Uuid::new_v4().to_string();
|
|
c.set_workspace_publish_namespace(&workspace_id.to_string(), &namespace)
|
|
.await
|
|
.unwrap();
|
|
|
|
let got_namespace = c
|
|
.get_workspace_publish_namespace(&workspace_id.to_string())
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(got_namespace, namespace);
|
|
}
|
|
{
|
|
// cannot set namespace too short
|
|
let err = c
|
|
.set_workspace_publish_namespace(&workspace_id.to_string(), "a") // too short
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert_eq!(format!("{:?}", err.code), "InvalidRequest");
|
|
}
|
|
|
|
{
|
|
// cannot set namespace with invalid chars
|
|
let err = c
|
|
.set_workspace_publish_namespace(&workspace_id.to_string(), "/|(*&)(&#@!") // invalid chars
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert_eq!(format!("{:?}", err.code), "InvalidRequest");
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_publish_doc() {
|
|
let (c, _user) = generate_unique_registered_user_client().await;
|
|
let workspace_id = get_first_workspace_string(&c).await;
|
|
let my_namespace = uuid::Uuid::new_v4().to_string();
|
|
c.set_workspace_publish_namespace(&workspace_id.to_string(), &my_namespace)
|
|
.await
|
|
.unwrap();
|
|
|
|
let publish_name_1 = "publish-name-1";
|
|
let view_id_1 = uuid::Uuid::new_v4();
|
|
let publish_name_2 = "publish-name-2";
|
|
let view_id_2 = uuid::Uuid::new_v4();
|
|
c.publish_collabs::<MyCustomMetadata, &[u8]>(
|
|
&workspace_id,
|
|
vec![
|
|
PublishCollabItem {
|
|
meta: PublishCollabMetadata {
|
|
view_id: view_id_1,
|
|
publish_name: publish_name_1.to_string(),
|
|
metadata: MyCustomMetadata {
|
|
title: "my_title_1".to_string(),
|
|
},
|
|
},
|
|
data: "yrs_encoded_data_1".as_bytes(),
|
|
},
|
|
PublishCollabItem {
|
|
meta: PublishCollabMetadata {
|
|
view_id: view_id_2,
|
|
publish_name: publish_name_2.to_string(),
|
|
metadata: MyCustomMetadata {
|
|
title: "my_title_2".to_string(),
|
|
},
|
|
},
|
|
data: "yrs_encoded_data_2".as_bytes(),
|
|
},
|
|
],
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
{
|
|
// Non login user should be able to view the published collab
|
|
let guest_client = localhost_client();
|
|
let published_collab = guest_client
|
|
.get_published_collab::<MyCustomMetadata>(&my_namespace, publish_name_1)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(published_collab.title, "my_title_1");
|
|
|
|
let publish_info = guest_client
|
|
.get_published_collab_info(&view_id_1)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(publish_info.namespace, Some(my_namespace.clone()));
|
|
assert_eq!(publish_info.publish_name, publish_name_1);
|
|
assert_eq!(publish_info.view_id, view_id_1);
|
|
|
|
let blob = guest_client
|
|
.get_published_collab_blob(&my_namespace, publish_name_1)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(blob, "yrs_encoded_data_1");
|
|
|
|
let publish_info = guest_client
|
|
.get_published_collab_info(&view_id_2)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(publish_info.namespace, Some(my_namespace.clone()));
|
|
assert_eq!(publish_info.publish_name, publish_name_2);
|
|
assert_eq!(publish_info.view_id, view_id_2);
|
|
|
|
let blob = guest_client
|
|
.get_published_collab_blob(&my_namespace, publish_name_2)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(blob, "yrs_encoded_data_2");
|
|
}
|
|
|
|
// updates data
|
|
c.publish_collabs::<MyCustomMetadata, &[u8]>(
|
|
&workspace_id,
|
|
vec![
|
|
PublishCollabItem {
|
|
meta: PublishCollabMetadata {
|
|
view_id: view_id_1,
|
|
publish_name: publish_name_1.to_string(),
|
|
metadata: MyCustomMetadata {
|
|
title: "my_title_1".to_string(),
|
|
},
|
|
},
|
|
data: "yrs_encoded_data_3".as_bytes(),
|
|
},
|
|
PublishCollabItem {
|
|
meta: PublishCollabMetadata {
|
|
view_id: view_id_2,
|
|
publish_name: publish_name_2.to_string(),
|
|
metadata: MyCustomMetadata {
|
|
title: "my_title_2".to_string(),
|
|
},
|
|
},
|
|
data: "yrs_encoded_data_4".as_bytes(),
|
|
},
|
|
],
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
{
|
|
// should see updated data
|
|
let guest_client = localhost_client();
|
|
let blob = guest_client
|
|
.get_published_collab_blob(&my_namespace, publish_name_1)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(blob, "yrs_encoded_data_3");
|
|
|
|
let blob = guest_client
|
|
.get_published_collab_blob(&my_namespace, publish_name_2)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(blob, "yrs_encoded_data_4");
|
|
}
|
|
|
|
c.unpublish_collabs(&workspace_id, &[view_id_1])
|
|
.await
|
|
.unwrap();
|
|
|
|
{
|
|
// Deleted collab should not be accessible
|
|
let guest_client = localhost_client();
|
|
let err = guest_client
|
|
.get_published_collab::<MyCustomMetadata>(&my_namespace, publish_name_1)
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert_eq!(format!("{:?}", err.code), "RecordNotFound");
|
|
|
|
let guest_client = localhost_client();
|
|
let err = guest_client
|
|
.get_published_collab_blob(&my_namespace, publish_name_1)
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert_eq!(format!("{:?}", err.code), "RecordNotFound");
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_publish_comments() {
|
|
let (page_owner_client, page_owner) = generate_unique_registered_user_client().await;
|
|
let workspace_id = get_first_workspace_string(&page_owner_client).await;
|
|
let published_view_namespace = uuid::Uuid::new_v4().to_string();
|
|
page_owner_client
|
|
.set_workspace_publish_namespace(&workspace_id.to_string(), &published_view_namespace)
|
|
.await
|
|
.unwrap();
|
|
|
|
let publish_name = "published-view";
|
|
let view_id = uuid::Uuid::new_v4();
|
|
page_owner_client
|
|
.publish_collabs::<MyCustomMetadata, &[u8]>(
|
|
&workspace_id,
|
|
vec![PublishCollabItem {
|
|
meta: PublishCollabMetadata {
|
|
view_id,
|
|
publish_name: publish_name.to_string(),
|
|
metadata: MyCustomMetadata {
|
|
title: "some_title".to_string(),
|
|
},
|
|
},
|
|
data: "yrs_encoded_data_1".as_bytes(),
|
|
}],
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
// Test if only authenticated users can create
|
|
let page_owner_comment_content = "comment from page owner";
|
|
page_owner_client
|
|
.create_comment_on_published_view(&view_id, page_owner_comment_content, &None)
|
|
.await
|
|
.unwrap();
|
|
let (first_user_client, first_user) = generate_unique_registered_user_client().await;
|
|
let first_user_comment_content = "comment from first authenticated user";
|
|
// This is to ensure that the second comment creation timestamp is later than the first one
|
|
sleep(Duration::from_millis(1));
|
|
first_user_client
|
|
.create_comment_on_published_view(&view_id, first_user_comment_content, &None)
|
|
.await
|
|
.unwrap();
|
|
let guest_client = localhost_client();
|
|
let result = guest_client
|
|
.create_comment_on_published_view(&view_id, "comment from anonymous", &None)
|
|
.await;
|
|
assert!(result.is_err());
|
|
assert_eq!(result.unwrap_err().code, ErrorCode::NotLoggedIn);
|
|
|
|
// Test if only all users, authenticated or not, can view all the comments
|
|
let published_view_comments: Vec<GlobalComment> = page_owner_client
|
|
.get_published_view_comments(&view_id)
|
|
.await
|
|
.unwrap()
|
|
.comments;
|
|
assert_eq!(published_view_comments.len(), 2);
|
|
let published_view_comments: Vec<GlobalComment> = first_user_client
|
|
.get_published_view_comments(&view_id)
|
|
.await
|
|
.unwrap()
|
|
.comments;
|
|
assert_eq!(published_view_comments.len(), 2);
|
|
let published_view_comments: Vec<GlobalComment> = guest_client
|
|
.get_published_view_comments(&view_id)
|
|
.await
|
|
.unwrap()
|
|
.comments;
|
|
assert_eq!(published_view_comments.len(), 2);
|
|
assert!(published_view_comments.iter().all(|c| !c.is_deleted));
|
|
|
|
// Test if the comments are correctly sorted
|
|
let comment_creators = published_view_comments
|
|
.iter()
|
|
.map(|c| {
|
|
c.user
|
|
.as_ref()
|
|
.map(|u| u.name.clone())
|
|
.unwrap_or("".to_string())
|
|
})
|
|
.collect_vec();
|
|
assert_eq!(
|
|
comment_creators,
|
|
vec![first_user.email.clone(), page_owner.email.clone()]
|
|
);
|
|
let comment_content = published_view_comments
|
|
.iter()
|
|
.map(|c| c.content.clone())
|
|
.collect_vec();
|
|
assert_eq!(
|
|
comment_content,
|
|
vec![first_user_comment_content, page_owner_comment_content]
|
|
);
|
|
|
|
// Test if it's possible to reply to another user's comment
|
|
let second_user_comment_content = "comment from second authenticated user";
|
|
let (second_user_client, second_user) = generate_unique_registered_user_client().await;
|
|
// User 2 reply to user 1
|
|
second_user_client
|
|
.create_comment_on_published_view(
|
|
&view_id,
|
|
second_user_comment_content,
|
|
&Some(published_view_comments[0].comment_id),
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let published_view_comments: Vec<GlobalComment> = guest_client
|
|
.get_published_view_comments(&view_id)
|
|
.await
|
|
.unwrap()
|
|
.comments;
|
|
let comment_creators = published_view_comments
|
|
.iter()
|
|
.map(|c| {
|
|
c.user
|
|
.as_ref()
|
|
.map(|u| u.name.clone())
|
|
.unwrap_or("".to_string())
|
|
})
|
|
.collect_vec();
|
|
assert_eq!(
|
|
comment_creators,
|
|
vec![
|
|
second_user.email.clone(),
|
|
first_user.email.clone(),
|
|
page_owner.email.clone()
|
|
]
|
|
);
|
|
assert_eq!(
|
|
published_view_comments[0].reply_comment_id,
|
|
Some(published_view_comments[1].comment_id)
|
|
);
|
|
|
|
// Test if only the page owner or the comment creator can delete a comment
|
|
// User 1 attempt to delete page owner's comment
|
|
let result = first_user_client
|
|
.delete_comment_on_published_view(&view_id, &published_view_comments[2].comment_id)
|
|
.await;
|
|
assert!(result.is_err());
|
|
assert_eq!(result.unwrap_err().code, ErrorCode::UserUnAuthorized);
|
|
// User 1 deletes own comment
|
|
first_user_client
|
|
.delete_comment_on_published_view(&view_id, &published_view_comments[1].comment_id)
|
|
.await
|
|
.unwrap();
|
|
// Guest client attempt to delete user 2's comment
|
|
let result = guest_client
|
|
.delete_comment_on_published_view(&view_id, &published_view_comments[0].comment_id)
|
|
.await;
|
|
assert!(result.is_err());
|
|
assert_eq!(result.unwrap_err().code, ErrorCode::NotLoggedIn);
|
|
// Verify that the comments are not deleted from the database, only the is_deleted status changes.
|
|
let published_view_comments: Vec<GlobalComment> = guest_client
|
|
.get_published_view_comments(&view_id)
|
|
.await
|
|
.unwrap()
|
|
.comments;
|
|
assert_eq!(
|
|
published_view_comments
|
|
.iter()
|
|
.map(|c| c.is_deleted)
|
|
.collect_vec(),
|
|
vec![false, true, false]
|
|
);
|
|
// Verify that the reference id is still preserved
|
|
assert_eq!(
|
|
published_view_comments[0].reply_comment_id,
|
|
Some(published_view_comments[1].comment_id)
|
|
);
|
|
|
|
for comment in &published_view_comments {
|
|
page_owner_client
|
|
.delete_comment_on_published_view(&view_id, &comment.comment_id)
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
let published_view_comments: Vec<GlobalComment> = guest_client
|
|
.get_published_view_comments(&view_id)
|
|
.await
|
|
.unwrap()
|
|
.comments;
|
|
assert_eq!(published_view_comments.len(), 3);
|
|
assert!(published_view_comments.iter().all(|c| c.is_deleted));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_publish_reactions() {
|
|
let (page_owner_client, _) = generate_unique_registered_user_client().await;
|
|
let workspace_id = get_first_workspace_string(&page_owner_client).await;
|
|
let published_view_namespace = uuid::Uuid::new_v4().to_string();
|
|
page_owner_client
|
|
.set_workspace_publish_namespace(&workspace_id.to_string(), &published_view_namespace)
|
|
.await
|
|
.unwrap();
|
|
|
|
let publish_name = "published-view";
|
|
let view_id = uuid::Uuid::new_v4();
|
|
page_owner_client
|
|
.publish_collabs::<MyCustomMetadata, &[u8]>(
|
|
&workspace_id,
|
|
vec![PublishCollabItem {
|
|
meta: PublishCollabMetadata {
|
|
view_id,
|
|
publish_name: publish_name.to_string(),
|
|
metadata: MyCustomMetadata {
|
|
title: "some_title".to_string(),
|
|
},
|
|
},
|
|
data: "yrs_encoded_data_1".as_bytes(),
|
|
}],
|
|
)
|
|
.await
|
|
.unwrap();
|
|
page_owner_client
|
|
.create_comment_on_published_view(&view_id, "likable comment", &None)
|
|
.await
|
|
.unwrap();
|
|
// This is to ensure that the second comment creation timestamp is later than the first one
|
|
sleep(Duration::from_millis(1));
|
|
page_owner_client
|
|
.create_comment_on_published_view(&view_id, "party comment", &None)
|
|
.await
|
|
.unwrap();
|
|
let mut comments = page_owner_client
|
|
.get_published_view_comments(&view_id)
|
|
.await
|
|
.unwrap()
|
|
.comments;
|
|
comments.sort_by_key(|c| c.created_at);
|
|
// Test if the reactions are created correctly based on view and comment id
|
|
let likable_comment_id = comments[0].comment_id;
|
|
let party_comment_id = comments[1].comment_id;
|
|
|
|
let like_emoji = "👍";
|
|
let party_emoji = "🎉";
|
|
page_owner_client
|
|
.create_reaction_on_comment(like_emoji, &view_id, &likable_comment_id)
|
|
.await
|
|
.unwrap();
|
|
let guest_client = localhost_client();
|
|
let result = guest_client
|
|
.create_reaction_on_comment(like_emoji, &view_id, &likable_comment_id)
|
|
.await;
|
|
assert!(result.is_err());
|
|
assert_eq!(result.unwrap_err().code, ErrorCode::NotLoggedIn);
|
|
|
|
let (user_client, _) = generate_unique_registered_user_client().await;
|
|
sleep(Duration::from_millis(1));
|
|
user_client
|
|
.create_reaction_on_comment(party_emoji, &view_id, &party_comment_id)
|
|
.await
|
|
.unwrap();
|
|
user_client
|
|
.create_reaction_on_comment(like_emoji, &view_id, &likable_comment_id)
|
|
.await
|
|
.unwrap();
|
|
|
|
let reactions = guest_client
|
|
.get_published_view_reactions(&view_id, &None)
|
|
.await
|
|
.unwrap()
|
|
.reactions;
|
|
assert_eq!(reactions[0].reaction_type, like_emoji);
|
|
assert_eq!(reactions[1].reaction_type, party_emoji);
|
|
let reaction_count: HashMap<String, i32> = reactions
|
|
.iter()
|
|
.map(|r| (r.reaction_type.clone(), r.react_users.len() as i32))
|
|
.collect();
|
|
assert_eq!(reaction_count.len(), 2);
|
|
assert_eq!(*reaction_count.get(like_emoji).unwrap(), 2);
|
|
assert_eq!(*reaction_count.get(party_emoji).unwrap(), 1);
|
|
|
|
// Test if the reactions are deleted correctly based on view and comment id
|
|
let result = guest_client
|
|
.delete_reaction_on_comment(like_emoji, &view_id, &likable_comment_id)
|
|
.await;
|
|
assert!(result.is_err());
|
|
assert_eq!(result.unwrap_err().code, ErrorCode::NotLoggedIn);
|
|
user_client
|
|
.delete_reaction_on_comment(like_emoji, &view_id, &likable_comment_id)
|
|
.await
|
|
.unwrap();
|
|
|
|
let reactions = guest_client
|
|
.get_published_view_reactions(&view_id, &None)
|
|
.await
|
|
.unwrap()
|
|
.reactions;
|
|
let reaction_count: HashMap<String, i32> = reactions
|
|
.iter()
|
|
.map(|r| (r.reaction_type.clone(), r.react_users.len() as i32))
|
|
.collect();
|
|
assert_eq!(reaction_count.len(), 2);
|
|
assert_eq!(*reaction_count.get(like_emoji).unwrap(), 1);
|
|
assert_eq!(*reaction_count.get(party_emoji).unwrap(), 1);
|
|
|
|
// Test if we can filter the reactions by comment id
|
|
let reactions = guest_client
|
|
.get_published_view_reactions(&view_id, &Some(likable_comment_id))
|
|
.await
|
|
.unwrap()
|
|
.reactions;
|
|
let reaction_count: HashMap<String, i32> = reactions
|
|
.iter()
|
|
.map(|r| (r.reaction_type.clone(), r.react_users.len() as i32))
|
|
.collect();
|
|
assert_eq!(reaction_count.len(), 1);
|
|
assert_eq!(*reaction_count.get(like_emoji).unwrap(), 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_publish_load_test() {
|
|
let (c, _user) = generate_unique_registered_user_client().await;
|
|
let workspace_id = get_first_workspace_string(&c).await;
|
|
let my_namespace = uuid::Uuid::new_v4().to_string();
|
|
c.set_workspace_publish_namespace(&workspace_id.to_string(), &my_namespace)
|
|
.await
|
|
.unwrap();
|
|
|
|
// publish nothing
|
|
c.publish_collabs::<(), &[u8]>(&workspace_id, vec![])
|
|
.await
|
|
.unwrap();
|
|
|
|
// publish nothing with metadata
|
|
c.publish_collabs::<MyCustomMetadata, &[u8]>(&workspace_id, vec![])
|
|
.await
|
|
.unwrap();
|
|
|
|
// publish 1000 collabs
|
|
let collabs: Vec<PublishCollabItem<MyCustomMetadata, Vec<u8>>> = (0..1000)
|
|
.map(|i| PublishCollabItem {
|
|
meta: PublishCollabMetadata {
|
|
view_id: uuid::Uuid::new_v4(),
|
|
publish_name: format!("publish-name-{}", i),
|
|
metadata: MyCustomMetadata {
|
|
title: format!("title_{}", i),
|
|
},
|
|
},
|
|
data: vec![0; 100_000], // 100 KB
|
|
})
|
|
.collect();
|
|
|
|
c.publish_collabs(&workspace_id, collabs).await.unwrap();
|
|
}
|
|
|
|
async fn get_first_workspace_string(c: &client_api::Client) -> String {
|
|
c.get_workspaces()
|
|
.await
|
|
.unwrap()
|
|
.first()
|
|
.unwrap()
|
|
.workspace_id
|
|
.to_string()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn workspace_member_publish_unpublish() {
|
|
let client_1 = TestClient::new_user_without_ws_conn().await;
|
|
let workspace_id = client_1.workspace_id().await;
|
|
let client_2 = TestClient::new_user_without_ws_conn().await;
|
|
client_1
|
|
.invite_and_accepted_workspace_member(&workspace_id, &client_2, AFRole::Member)
|
|
.await
|
|
.unwrap();
|
|
|
|
let view_id = uuid::Uuid::new_v4();
|
|
// member can publish without owner setting namespace
|
|
client_2
|
|
.api_client
|
|
.publish_collabs::<MyCustomMetadata, &[u8]>(
|
|
&workspace_id,
|
|
vec![PublishCollabItem {
|
|
meta: PublishCollabMetadata {
|
|
view_id,
|
|
publish_name: "publish-name-1".to_string(),
|
|
metadata: MyCustomMetadata {
|
|
title: "my_title_1".to_string(),
|
|
},
|
|
},
|
|
data: "yrs_encoded_data_1".as_bytes(),
|
|
}],
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
client_2
|
|
.api_client
|
|
.unpublish_collabs(&workspace_id, &[view_id])
|
|
.await
|
|
.unwrap();
|
|
}
|
|
|
|
#[derive(serde::Serialize, serde::Deserialize)]
|
|
struct MyCustomMetadata {
|
|
title: String,
|
|
}
|