370 lines
9.7 KiB
Rust
370 lines
9.7 KiB
Rust
use crate::collab::util::generate_random_string;
|
|
use client_api::collab_sync::NUMBER_OF_UPDATE_TRIGGER_INIT_SYNC;
|
|
use client_api_test_util::*;
|
|
use collab_entity::CollabType;
|
|
use database_entity::dto::{AFAccessLevel, QueryCollabParams};
|
|
use serde_json::json;
|
|
use sqlx::types::uuid;
|
|
use std::time::Duration;
|
|
use tokio::time::sleep;
|
|
use tracing::trace;
|
|
|
|
#[tokio::test]
|
|
async fn sync_collab_content_after_reconnect_test() {
|
|
let object_id = uuid::Uuid::new_v4().to_string();
|
|
let collab_type = CollabType::Document;
|
|
|
|
let mut test_client = TestClient::new_user().await;
|
|
let workspace_id = test_client.workspace_id().await;
|
|
test_client
|
|
.open_collab(&workspace_id, &object_id, collab_type.clone())
|
|
.await;
|
|
|
|
// Disconnect the client and edit the collab. The updates will not be sent to the server.
|
|
test_client.disconnect().await;
|
|
for i in 0..=5 {
|
|
test_client
|
|
.collab_by_object_id
|
|
.get_mut(&object_id)
|
|
.unwrap()
|
|
.collab
|
|
.lock()
|
|
.insert(&i.to_string(), i.to_string());
|
|
}
|
|
|
|
// it will return RecordNotFound error when trying to get the collab from the server
|
|
let err = test_client
|
|
.api_client
|
|
.get_collab(QueryCollabParams::new(
|
|
&object_id,
|
|
collab_type.clone(),
|
|
&workspace_id,
|
|
))
|
|
.await
|
|
.unwrap_err();
|
|
assert!(err.is_record_not_found());
|
|
|
|
// After reconnect the collab should be synced to the server.
|
|
test_client.reconnect().await;
|
|
// Wait for the messages to be sent
|
|
test_client.wait_object_sync_complete(&object_id).await;
|
|
|
|
assert_server_collab(
|
|
&workspace_id,
|
|
&mut test_client.api_client,
|
|
&object_id,
|
|
&collab_type,
|
|
10,
|
|
json!( {
|
|
"0": "0",
|
|
"1": "1",
|
|
"2": "2",
|
|
"3": "3",
|
|
"4": "4",
|
|
"5": "5",
|
|
}),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn same_client_with_diff_devices_edit_same_collab_test() {
|
|
let collab_type = CollabType::Document;
|
|
let registered_user = generate_unique_registered_user().await;
|
|
let mut client_1 = TestClient::user_with_new_device(registered_user.clone()).await;
|
|
let mut client_2 = TestClient::user_with_new_device(registered_user.clone()).await;
|
|
|
|
let workspace_id = client_1.workspace_id().await;
|
|
let object_id = client_1
|
|
.create_and_edit_collab(&workspace_id, collab_type.clone())
|
|
.await;
|
|
|
|
// client 1 edit the collab
|
|
client_1
|
|
.collab_by_object_id
|
|
.get_mut(&object_id)
|
|
.unwrap()
|
|
.collab
|
|
.lock()
|
|
.insert("name", "work");
|
|
client_1.wait_object_sync_complete(&object_id).await;
|
|
|
|
client_2
|
|
.open_collab(&workspace_id, &object_id, collab_type.clone())
|
|
.await;
|
|
sleep(Duration::from_millis(1000)).await;
|
|
|
|
trace!("client 2 disconnect: {:?}", client_2.device_id);
|
|
client_2.disconnect().await;
|
|
sleep(Duration::from_millis(1000)).await;
|
|
|
|
client_2
|
|
.collab_by_object_id
|
|
.get_mut(&object_id)
|
|
.unwrap()
|
|
.collab
|
|
.lock()
|
|
.insert("name", "workspace");
|
|
|
|
client_2.reconnect().await;
|
|
client_2.wait_object_sync_complete(&object_id).await;
|
|
|
|
let expected_json = json!({
|
|
"name": "workspace"
|
|
});
|
|
assert_client_collab_within_30_secs(&mut client_1, &object_id, "name", expected_json.clone())
|
|
.await;
|
|
assert_client_collab_within_30_secs(&mut client_2, &object_id, "name", expected_json.clone())
|
|
.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn same_client_with_diff_devices_edit_diff_collab_test() {
|
|
let registered_user = generate_unique_registered_user().await;
|
|
let collab_type = CollabType::Document;
|
|
let mut device_1 = TestClient::user_with_new_device(registered_user.clone()).await;
|
|
let mut device_2 = TestClient::user_with_new_device(registered_user.clone()).await;
|
|
|
|
let workspace_id = device_1.workspace_id().await;
|
|
|
|
// different devices create different collabs. the collab will be synced between devices
|
|
let object_id_1 = device_1
|
|
.create_and_edit_collab(&workspace_id, collab_type.clone())
|
|
.await;
|
|
let object_id_2 = device_2
|
|
.create_and_edit_collab(&workspace_id, collab_type.clone())
|
|
.await;
|
|
|
|
// client 1 edit the collab with object_id_1
|
|
device_1
|
|
.collab_by_object_id
|
|
.get_mut(&object_id_1)
|
|
.unwrap()
|
|
.collab
|
|
.lock()
|
|
.insert("name", "object 1");
|
|
device_1.wait_object_sync_complete(&object_id_1).await;
|
|
|
|
// client 2 edit the collab with object_id_2
|
|
device_2
|
|
.collab_by_object_id
|
|
.get_mut(&object_id_2)
|
|
.unwrap()
|
|
.collab
|
|
.lock()
|
|
.insert("name", "object 2");
|
|
device_2.wait_object_sync_complete(&object_id_2).await;
|
|
|
|
// client1 open the collab with object_id_2
|
|
device_1
|
|
.open_collab(&workspace_id, &object_id_2, collab_type.clone())
|
|
.await;
|
|
assert_client_collab_within_30_secs(
|
|
&mut device_1,
|
|
&object_id_2,
|
|
"name",
|
|
json!({
|
|
"name": "object 2"
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// client2 open the collab with object_id_1
|
|
device_2
|
|
.open_collab(&workspace_id, &object_id_1, collab_type.clone())
|
|
.await;
|
|
assert_client_collab_within_30_secs(
|
|
&mut device_2,
|
|
&object_id_1,
|
|
"name",
|
|
json!({
|
|
"name": "object 1"
|
|
}),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn edit_document_with_both_clients_offline_then_online_sync_test() {
|
|
let collab_type = CollabType::Document;
|
|
let mut client_1 = TestClient::new_user().await;
|
|
let mut client_2 = TestClient::new_user().await;
|
|
|
|
let workspace_id = client_1.workspace_id().await;
|
|
let object_id = client_1
|
|
.create_and_edit_collab(&workspace_id, collab_type.clone())
|
|
.await;
|
|
|
|
// add client 2 as a member of the collab
|
|
client_1
|
|
.add_client_as_collab_member(
|
|
&workspace_id,
|
|
&object_id,
|
|
&client_2,
|
|
AFAccessLevel::ReadAndWrite,
|
|
)
|
|
.await;
|
|
client_1.disconnect().await;
|
|
|
|
client_2
|
|
.open_collab(&workspace_id, &object_id, collab_type.clone())
|
|
.await;
|
|
client_2.disconnect().await;
|
|
|
|
for i in 0..10 {
|
|
if i % 2 == 0 {
|
|
client_1
|
|
.collab_by_object_id
|
|
.get_mut(&object_id)
|
|
.unwrap()
|
|
.collab
|
|
.lock()
|
|
.insert(&i.to_string(), format!("Task {}", i));
|
|
} else {
|
|
client_2
|
|
.collab_by_object_id
|
|
.get_mut(&object_id)
|
|
.unwrap()
|
|
.collab
|
|
.lock()
|
|
.insert(&i.to_string(), format!("Task {}", i));
|
|
}
|
|
}
|
|
|
|
tokio::join!(client_1.reconnect(), client_2.reconnect());
|
|
tokio::join!(
|
|
client_1.wait_object_sync_complete(&object_id),
|
|
client_2.wait_object_sync_complete(&object_id)
|
|
);
|
|
|
|
let expected_json = json!({
|
|
"0": "Task 0",
|
|
"1": "Task 1",
|
|
"2": "Task 2",
|
|
"3": "Task 3",
|
|
"4": "Task 4",
|
|
"5": "Task 5",
|
|
"6": "Task 6",
|
|
"7": "Task 7",
|
|
"8": "Task 8",
|
|
"9": "Task 9"
|
|
});
|
|
assert_client_collab_include_value_within_30_secs(
|
|
&mut client_1,
|
|
&object_id,
|
|
expected_json.clone(),
|
|
)
|
|
.await;
|
|
assert_client_collab_include_value_within_30_secs(
|
|
&mut client_2,
|
|
&object_id,
|
|
expected_json.clone(),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn init_sync_when_missing_updates_test() {
|
|
let text = generate_random_string(1024);
|
|
let collab_type = CollabType::Document;
|
|
let mut client_1 = TestClient::new_user().await;
|
|
let mut client_2 = TestClient::new_user().await;
|
|
|
|
// Create a collaborative document with client_1 and invite client_2 to collaborate.
|
|
let workspace_id = client_1.workspace_id().await;
|
|
let object_id = client_1
|
|
.create_and_edit_collab(&workspace_id, collab_type.clone())
|
|
.await;
|
|
client_1
|
|
.add_client_as_collab_member(
|
|
&workspace_id,
|
|
&object_id,
|
|
&client_2,
|
|
AFAccessLevel::ReadAndWrite,
|
|
)
|
|
.await;
|
|
|
|
// Client_1 makes the first edit by inserting "task 1".
|
|
client_1
|
|
.collab_by_object_id
|
|
.get_mut(&object_id)
|
|
.unwrap()
|
|
.collab
|
|
.lock()
|
|
.insert("1", "task 1");
|
|
client_1.wait_object_sync_complete(&object_id).await;
|
|
|
|
// Client_2 opens the collaboration, triggering an initial sync to receive "task 1".
|
|
client_2
|
|
.open_collab(&workspace_id, &object_id, collab_type.clone())
|
|
.await;
|
|
client_2.wait_object_sync_complete(&object_id).await;
|
|
|
|
// Validate both clients have "task 1" after the initial sync.
|
|
assert_eq!(
|
|
client_1.get_edit_collab_json(&object_id).await,
|
|
json!({ "1": "task 1" })
|
|
);
|
|
assert_eq!(
|
|
client_2.get_edit_collab_json(&object_id).await,
|
|
json!({ "1": "task 1" })
|
|
);
|
|
|
|
// Simulate client_2 missing updates by enabling skip_realtime_message.
|
|
client_2.ws_client.disable_receive_message();
|
|
client_1.wait_object_sync_complete(&object_id).await;
|
|
|
|
// Client_1 inserts "task 2", which client_2 misses due to skipping realtime messages.
|
|
for _ in 0..NUMBER_OF_UPDATE_TRIGGER_INIT_SYNC {
|
|
client_1
|
|
.collab_by_object_id
|
|
.get_mut(&object_id)
|
|
.unwrap()
|
|
.collab
|
|
.lock()
|
|
.insert("2", text.clone());
|
|
sleep(Duration::from_millis(300)).await;
|
|
}
|
|
client_1.wait_object_sync_complete(&object_id).await;
|
|
|
|
client_2
|
|
.collab_by_object_id
|
|
.get_mut(&object_id)
|
|
.unwrap()
|
|
.collab
|
|
.lock()
|
|
.insert("3", "task 3");
|
|
client_2.wait_object_sync_complete(&object_id).await;
|
|
|
|
// Validate client_1's view includes "task 2", and "task 3", while client_2 missed key2 and key3.
|
|
assert_client_collab_include_value_within_30_secs(
|
|
&mut client_1,
|
|
&object_id,
|
|
json!({ "1": "task 1", "2": text.clone(), "3": "task 3" }),
|
|
)
|
|
.await;
|
|
assert_eq!(
|
|
client_2.get_edit_collab_json(&object_id).await,
|
|
json!({ "1": "task 1", "3": "task 3" })
|
|
);
|
|
|
|
// client_2 resumes receiving messages
|
|
// client_1 triggers a sync that will trigger broadcast message to client_2
|
|
client_2.ws_client.enable_receive_message();
|
|
client_1
|
|
.collab_by_object_id
|
|
.get_mut(&object_id)
|
|
.unwrap()
|
|
.collab
|
|
.lock()
|
|
.insert("4", "task 4");
|
|
client_1.wait_object_sync_complete(&object_id).await;
|
|
|
|
assert_client_collab_include_value_within_30_secs(
|
|
&mut client_2,
|
|
&object_id,
|
|
json!({ "1": "task 1", "2": text.clone(), "3": "task 3", "4": "task 4" }),
|
|
)
|
|
.await;
|
|
}
|