Browse Source

refactor tests

appflowy 3 năm trước cách đây
mục cha
commit
7ac55f29db
30 tập tin đã thay đổi với 465 bổ sung452 xóa
  1. 1 0
      backend/Cargo.lock
  2. 10 10
      backend/tests/document/helper.rs
  3. 1 1
      frontend/rust-lib/flowy-core/tests/workspace/app_test.rs
  4. 6 6
      frontend/rust-lib/flowy-core/tests/workspace/view_test.rs
  5. 5 5
      frontend/rust-lib/flowy-core/tests/workspace/workspace_test.rs
  6. 1 0
      frontend/rust-lib/flowy-document/Cargo.toml
  7. 2 2
      frontend/rust-lib/flowy-document/src/errors.rs
  8. 1 1
      frontend/rust-lib/flowy-document/src/module.rs
  9. 1 1
      frontend/rust-lib/flowy-document/src/services/cache.rs
  10. 2 2
      frontend/rust-lib/flowy-document/src/services/doc/controller.rs
  11. 2 2
      frontend/rust-lib/flowy-document/src/services/doc/edit/editor.rs
  12. 2 2
      frontend/rust-lib/flowy-document/src/services/doc/edit/queue.rs
  13. 1 1
      frontend/rust-lib/flowy-document/src/services/doc/mod.rs
  14. 37 41
      frontend/rust-lib/flowy-document/src/services/doc/revision/cache.rs
  15. 2 2
      frontend/rust-lib/flowy-document/src/services/doc/revision/manager.rs
  16. 1 0
      frontend/rust-lib/flowy-document/src/services/doc/revision/sync.rs
  17. 1 0
      frontend/rust-lib/flowy-document/tests/editor/mod.rs
  18. 8 0
      frontend/rust-lib/flowy-document/tests/editor/revision_test.rs
  19. 22 0
      frontend/rust-lib/flowy-test/src/editor.rs
  20. 14 17
      frontend/rust-lib/flowy-test/src/event_builder.rs
  21. 269 6
      frontend/rust-lib/flowy-test/src/helper.rs
  22. 22 22
      frontend/rust-lib/flowy-test/src/lib.rs
  23. 0 276
      frontend/rust-lib/flowy-test/src/workspace.rs
  24. 12 12
      frontend/rust-lib/flowy-user/tests/event/auth_test.rs
  25. 1 1
      frontend/rust-lib/flowy-user/tests/event/helper.rs
  26. 28 22
      frontend/rust-lib/flowy-user/tests/event/user_profile_test.rs
  27. 3 0
      shared-lib/flowy-document-infra/src/entities/ws/ws.rs
  28. 2 0
      shared-lib/lib-ot/src/errors.rs
  29. 6 20
      shared-lib/lib-ot/src/revision/cache.rs
  30. 2 0
      shared-lib/lib-ot/src/revision/model.rs

+ 1 - 0
backend/Cargo.lock

@@ -1272,6 +1272,7 @@ dependencies = [
  "flowy-document-infra",
  "futures",
  "futures-core",
+ "futures-util",
  "lazy_static",
  "lib-dispatch",
  "lib-infra",

+ 10 - 10
backend/tests/document/helper.rs

@@ -3,7 +3,7 @@
 use actix_web::web::Data;
 use backend::services::doc::{crud::update_doc, manager::DocManager};
 use flowy_document::services::doc::edit::ClientDocEditor as ClientEditDocContext;
-use flowy_test::{workspace::ViewTest, FlowyTest};
+use flowy_test::{helper::ViewTest, FlowySDKTest};
 use flowy_user::services::user::UserSession;
 use futures_util::{stream, stream::StreamExt};
 use sqlx::PgPool;
@@ -18,7 +18,7 @@ use lib_ot::core::Interval;
 
 pub struct DocumentTest {
     server: TestServer,
-    flowy_test: FlowyTest,
+    flowy_test: FlowySDKTest,
 }
 #[derive(Clone)]
 pub enum DocScript {
@@ -34,7 +34,7 @@ pub enum DocScript {
 impl DocumentTest {
     pub async fn new() -> Self {
         let server = spawn_server().await;
-        let flowy_test = FlowyTest::setup_with(server.client_server_config.clone());
+        let flowy_test = FlowySDKTest::setup_with(server.client_server_config.clone());
         Self { server, flowy_test }
     }
 
@@ -50,7 +50,7 @@ impl DocumentTest {
 #[derive(Clone)]
 struct ScriptContext {
     client_edit_context: Option<Arc<ClientEditDocContext>>,
-    flowy_test: FlowyTest,
+    client_sdk: FlowySDKTest,
     client_user_session: Arc<UserSession>,
     server_doc_manager: Arc<DocManager>,
     server_pg_pool: Data<PgPool>,
@@ -58,13 +58,13 @@ struct ScriptContext {
 }
 
 impl ScriptContext {
-    async fn new(flowy_test: FlowyTest, server: TestServer) -> Self {
-        let user_session = flowy_test.sdk.user_session.clone();
-        let doc_id = create_doc(&flowy_test).await;
+    async fn new(client_sdk: FlowySDKTest, server: TestServer) -> Self {
+        let user_session = client_sdk.user_session.clone();
+        let doc_id = create_doc(&client_sdk).await;
 
         Self {
             client_edit_context: None,
-            flowy_test,
+            client_sdk,
             client_user_session: user_session,
             server_doc_manager: server.app_ctx.doc_biz.manager.clone(),
             server_pg_pool: Data::new(server.pg_pool.clone()),
@@ -73,7 +73,7 @@ impl ScriptContext {
     }
 
     async fn open_doc(&mut self) {
-        let flowy_document = self.flowy_test.sdk.flowy_document.clone();
+        let flowy_document = self.client_sdk.flowy_document.clone();
         let doc_id = self.doc_id.clone();
 
         let edit_context = flowy_document.open(DocIdentifier { doc_id }).await.unwrap();
@@ -161,7 +161,7 @@ fn assert_eq(expect: &str, receive: &str) {
     assert_eq!(target_delta, expected_delta);
 }
 
-async fn create_doc(flowy_test: &FlowyTest) -> String {
+async fn create_doc(flowy_test: &FlowySDKTest) -> String {
     let view_test = ViewTest::new(flowy_test).await;
     view_test.view.id
 }

+ 1 - 1
frontend/rust-lib/flowy-core/tests/workspace/app_test.rs

@@ -3,7 +3,7 @@ use flowy_core::entities::{
     trash::{TrashIdentifier, TrashType},
     view::*,
 };
-use flowy_test::workspace::*;
+use flowy_test::helper::*;
 
 #[tokio::test]
 #[should_panic]

+ 6 - 6
frontend/rust-lib/flowy-core/tests/workspace/view_test.rs

@@ -3,12 +3,12 @@ use flowy_core::entities::{
     trash::{TrashIdentifier, TrashType},
     view::*,
 };
-use flowy_test::{workspace::*, FlowyTest};
+use flowy_test::{helper::*, FlowySDKTest};
 
 #[tokio::test]
 #[should_panic]
 async fn view_delete() {
-    let test = FlowyTest::setup();
+    let test = FlowySDKTest::setup();
     let _ = test.init_user().await;
 
     let test = ViewTest::new(&test).await;
@@ -21,7 +21,7 @@ async fn view_delete() {
 
 #[tokio::test]
 async fn view_delete_then_putback() {
-    let test = FlowyTest::setup();
+    let test = FlowySDKTest::setup();
     let _ = test.init_user().await;
 
     let test = ViewTest::new(&test).await;
@@ -44,7 +44,7 @@ async fn view_delete_then_putback() {
 
 #[tokio::test]
 async fn view_delete_all() {
-    let test = FlowyTest::setup();
+    let test = FlowySDKTest::setup();
     let _ = test.init_user().await;
 
     let test = ViewTest::new(&test).await;
@@ -66,7 +66,7 @@ async fn view_delete_all() {
 
 #[tokio::test]
 async fn view_delete_all_permanent() {
-    let test = FlowyTest::setup();
+    let test = FlowySDKTest::setup();
     let _ = test.init_user().await;
 
     let test = ViewTest::new(&test).await;
@@ -85,7 +85,7 @@ async fn view_delete_all_permanent() {
 
 #[tokio::test]
 async fn view_open_doc() {
-    let test = FlowyTest::setup();
+    let test = FlowySDKTest::setup();
     let _ = test.init_user().await;
 
     let test = ViewTest::new(&test).await;

+ 5 - 5
frontend/rust-lib/flowy-core/tests/workspace/workspace_test.rs

@@ -3,7 +3,7 @@ use flowy_core::{
     event::WorkspaceEvent::*,
     prelude::*,
 };
-use flowy_test::{builder::*, workspace::*, FlowyTest};
+use flowy_test::{event_builder::*, helper::*, FlowySDKTest};
 
 #[tokio::test]
 async fn workspace_read_all() {
@@ -42,13 +42,13 @@ async fn workspace_create_with_apps() {
 #[tokio::test]
 async fn workspace_create_with_invalid_name() {
     for (name, code) in invalid_workspace_name_test_case() {
-        let sdk = FlowyTest::setup().sdk;
+        let sdk = FlowySDKTest::setup();
         let request = CreateWorkspaceRequest {
             name,
             desc: "".to_owned(),
         };
         assert_eq!(
-            FlowyWorkspaceTest::new(sdk)
+            CoreModuleEventBuilder::new(sdk)
                 .event(CreateWorkspace)
                 .request(request)
                 .async_send()
@@ -62,14 +62,14 @@ async fn workspace_create_with_invalid_name() {
 
 #[tokio::test]
 async fn workspace_update_with_invalid_name() {
-    let sdk = FlowyTest::setup().sdk;
+    let sdk = FlowySDKTest::setup();
     for (name, code) in invalid_workspace_name_test_case() {
         let request = CreateWorkspaceRequest {
             name,
             desc: "".to_owned(),
         };
         assert_eq!(
-            FlowyWorkspaceTest::new(sdk.clone())
+            CoreModuleEventBuilder::new(sdk.clone())
                 .event(CreateWorkspace)
                 .request(request)
                 .async_send()

+ 1 - 0
frontend/rust-lib/flowy-document/Cargo.toml

@@ -39,6 +39,7 @@ serde = { version = "1.0", features = ["derive"] }
 serde_json = {version = "1.0"}
 chrono = "0.4.19"
 futures-core = { version = "0.3", default-features = false }
+futures-util = "0.3.15"
 byteorder = {version = "1.3.4"}
 async-stream = "0.3.2"
 futures = "0.3.15"

+ 2 - 2
frontend/rust-lib/flowy-document/src/errors.rs

@@ -46,7 +46,7 @@ impl DocError {
     static_doc_error!(ws, ErrorCode::WsConnectError);
     static_doc_error!(internal, ErrorCode::InternalError);
     static_doc_error!(unauthorized, ErrorCode::UserUnauthorized);
-    static_doc_error!(record_not_found, ErrorCode::DocNotfound);
+    static_doc_error!(doc_not_found, ErrorCode::DocNotfound);
     static_doc_error!(duplicate_rev, ErrorCode::DuplicateRevision);
 }
 
@@ -82,7 +82,7 @@ impl std::default::Default for ErrorCode {
 impl std::convert::From<flowy_database::Error> for DocError {
     fn from(error: flowy_database::Error) -> Self {
         match error {
-            flowy_database::Error::NotFound => DocError::record_not_found().context(error),
+            flowy_database::Error::NotFound => DocError::doc_not_found().context(error),
             _ => DocError::internal().context(error),
         }
     }

+ 1 - 1
frontend/rust-lib/flowy-document/src/module.rs

@@ -1,7 +1,7 @@
 use crate::{
     errors::DocError,
     services::{
-        doc::{doc_controller::DocController, edit::ClientDocEditor},
+        doc::{controller::DocController, edit::ClientDocEditor},
         server::construct_doc_server,
         ws::WsDocumentManager,
     },

+ 1 - 1
frontend/rust-lib/flowy-document/src/services/cache.rs

@@ -46,4 +46,4 @@ impl DocCache {
     }
 }
 
-fn doc_not_found() -> DocError { DocError::record_not_found().context("Doc is close or you should call open first") }
+fn doc_not_found() -> DocError { DocError::doc_not_found().context("Doc is close or you should call open first") }

+ 2 - 2
frontend/rust-lib/flowy-document/src/services/doc/doc_controller.rs → frontend/rust-lib/flowy-document/src/services/doc/controller.rs

@@ -122,7 +122,7 @@ struct RevisionServerImpl {
 
 impl RevisionServer for RevisionServerImpl {
     #[tracing::instrument(level = "debug", skip(self))]
-    fn fetch_document_from_remote(&self, doc_id: &str) -> ResultFuture<Doc, DocError> {
+    fn fetch_document(&self, doc_id: &str) -> ResultFuture<Doc, DocError> {
         let params = DocIdentifier {
             doc_id: doc_id.to_string(),
         };
@@ -131,7 +131,7 @@ impl RevisionServer for RevisionServerImpl {
 
         ResultFuture::new(async move {
             match server.read_doc(&token, params).await? {
-                None => Err(DocError::record_not_found().context("Remote doesn't have this document")),
+                None => Err(DocError::doc_not_found().context("Remote doesn't have this document")),
                 Some(doc) => Ok(doc),
             }
         })

+ 2 - 2
frontend/rust-lib/flowy-document/src/services/doc/edit/editor.rs

@@ -209,7 +209,7 @@ impl ClientDocEditor {
     async fn handle_push_rev(&self, bytes: Bytes) -> DocResult<()> {
         // Transform the revision
         let (ret, rx) = oneshot::channel::<DocumentResult<TransformDeltas>>();
-        let _ = self.edit_tx.send(EditCommand::RemoteRevision { bytes, ret });
+        let _ = self.edit_tx.send(EditCommand::ProcessRemoteRevision { bytes, ret });
         let TransformDeltas {
             client_prime,
             server_prime,
@@ -268,12 +268,12 @@ impl ClientDocEditor {
                 let revision = self.rev_manager.mk_revisions(range).await?;
                 let _ = self.ws.send(revision.into());
             },
-            WsDataType::NewDocUser => {},
             WsDataType::Acked => {
                 let rev_id = RevId::try_from(bytes)?;
                 let _ = self.rev_manager.ack_revision(rev_id).await?;
             },
             WsDataType::Conflict => {},
+            WsDataType::NewDocUser => {},
         }
         Ok(())
     }

+ 2 - 2
frontend/rust-lib/flowy-document/src/services/doc/edit/queue.rs

@@ -55,7 +55,7 @@ impl EditCommandQueue {
                 let result = self.composed_delta(delta).await;
                 let _ = ret.send(result);
             },
-            EditCommand::RemoteRevision { bytes, ret } => {
+            EditCommand::ProcessRemoteRevision { bytes, ret } => {
                 let revision = Revision::try_from(bytes)?;
                 let delta = RichTextDelta::from_bytes(&revision.delta_data)?;
                 let rev_id: RevId = revision.rev_id.into();
@@ -131,7 +131,7 @@ pub(crate) enum EditCommand {
         delta: RichTextDelta,
         ret: Ret<()>,
     },
-    RemoteRevision {
+    ProcessRemoteRevision {
         bytes: Bytes,
         ret: Ret<TransformDeltas>,
     },

+ 1 - 1
frontend/rust-lib/flowy-document/src/services/doc/mod.rs

@@ -1,4 +1,4 @@
 pub mod edit;
 pub mod revision;
 
-pub(crate) mod doc_controller;
+pub(crate) mod controller;

+ 37 - 41
frontend/rust-lib/flowy-document/src/services/doc/revision/cache.rs

@@ -22,7 +22,7 @@ use lib_ot::{
 };
 use std::{sync::Arc, time::Duration};
 use tokio::{
-    sync::RwLock,
+    sync::{mpsc, RwLock},
     task::{spawn_blocking, JoinHandle},
 };
 
@@ -107,13 +107,15 @@ impl RevisionCache {
         }
     }
 
-    pub async fn fetch_document(&self) -> DocResult<Doc> {
-        let result = fetch_from_local(&self.doc_id, self.dish_cache.clone()).await;
+    pub async fn load_document(&self) -> DocResult<Doc> {
+        // Loading the document from disk and it will be sync with server.
+        let result = load_from_disk(&self.doc_id, self.memory_cache.clone(), self.dish_cache.clone()).await;
         if result.is_ok() {
             return result;
         }
 
-        let doc = self.server.fetch_document_from_remote(&self.doc_id).await?;
+        // The document doesn't exist in local. Try load from server
+        let doc = self.server.fetch_document(&self.doc_id).await?;
         let delta_data = doc.data.as_bytes();
         let revision = Revision::new(
             doc.base_rev_id,
@@ -154,21 +156,30 @@ impl RevisionIterator for RevisionCache {
     }
 }
 
-async fn fetch_from_local(doc_id: &str, disk_cache: Arc<DocRevisionDeskCache>) -> DocResult<Doc> {
+async fn load_from_disk(
+    doc_id: &str,
+    memory_cache: Arc<RevisionMemoryCache>,
+    disk_cache: Arc<DocRevisionDeskCache>,
+) -> DocResult<Doc> {
     let doc_id = doc_id.to_owned();
-    spawn_blocking(move || {
+    let (tx, mut rx) = mpsc::channel(2);
+    let doc = spawn_blocking(move || {
         let revisions = disk_cache.read_revisions(&doc_id)?;
         if revisions.is_empty() {
-            return Err(DocError::record_not_found().context("Local doesn't have this document"));
+            return Err(DocError::doc_not_found().context("Local doesn't have this document"));
         }
 
-        let base_rev_id: RevId = revisions.last().unwrap().base_rev_id.into();
-        let rev_id: RevId = revisions.last().unwrap().rev_id.into();
+        let (base_rev_id, rev_id) = revisions.last().unwrap().pair_rev_id();
         let mut delta = RichTextDelta::new();
         for (_, revision) in revisions.into_iter().enumerate() {
-            match RichTextDelta::from_bytes(revision.delta_data) {
+            // Opti: revision's clone may cause memory issues
+            match RichTextDelta::from_bytes(revision.clone().delta_data) {
                 Ok(local_delta) => {
                     delta = delta.compose(&local_delta)?;
+                    match tx.blocking_send(revision) {
+                        Ok(_) => {},
+                        Err(e) => log::error!("Load document from disk error: {}", e),
+                    }
                 },
                 Err(e) => {
                     log::error!("Deserialize delta from revision failed: {}", e);
@@ -176,51 +187,36 @@ async fn fetch_from_local(doc_id: &str, disk_cache: Arc<DocRevisionDeskCache>) -
             }
         }
 
-        #[cfg(debug_assertions)]
-        validate_delta(&doc_id, disk_cache, &delta);
-
-        match delta.ops.last() {
-            None => {},
-            Some(op) => {
-                let data = op.get_data();
-                if !data.ends_with('\n') {
-                    delta.ops.push(Operation::Insert("\n".into()))
-                }
-            },
-        }
-
+        correct_delta_if_need(&mut delta);
         Result::<Doc, DocError>::Ok(Doc {
             id: doc_id,
             data: delta.to_json(),
-            rev_id: rev_id.into(),
-            base_rev_id: base_rev_id.into(),
+            rev_id,
+            base_rev_id,
         })
     })
     .await
-    .map_err(internal_error)?
+    .map_err(internal_error)?;
+
+    while let Some(revision) = rx.recv().await {
+        match memory_cache.add_revision(revision).await {
+            Ok(_) => {},
+            Err(e) => log::error!("{:?}", e),
+        }
+    }
+
+    doc
 }
 
-#[cfg(debug_assertions)]
-fn validate_delta(doc_id: &str, disk_cache: Arc<DocRevisionDeskCache>, delta: &RichTextDelta) {
+fn correct_delta_if_need(delta: &mut RichTextDelta) {
     if delta.ops.last().is_none() {
         return;
     }
 
     let data = delta.ops.last().as_ref().unwrap().get_data();
     if !data.ends_with('\n') {
-        log::error!("The op must end with newline");
-        let result = || {
-            let revisions = disk_cache.read_revisions(&doc_id)?;
-            for revision in revisions {
-                let delta = RichTextDelta::from_bytes(revision.delta_data)?;
-                log::error!("Invalid revision: {}:{}", revision.rev_id, delta.to_json());
-            }
-            Ok::<(), DocError>(())
-        };
-        match result() {
-            Ok(_) => {},
-            Err(e) => log::error!("{}", e),
-        }
+        log::error!("The op must end with newline. Correcting it by inserting newline op");
+        delta.ops.push(Operation::Insert("\n".into()));
     }
 }
 

+ 2 - 2
frontend/rust-lib/flowy-document/src/services/doc/revision/manager.rs

@@ -14,7 +14,7 @@ use std::sync::Arc;
 use tokio::sync::mpsc;
 
 pub trait RevisionServer: Send + Sync {
-    fn fetch_document_from_remote(&self, doc_id: &str) -> ResultFuture<Doc, DocError>;
+    fn fetch_document(&self, doc_id: &str) -> ResultFuture<Doc, DocError>;
 }
 
 pub struct RevisionManager {
@@ -41,7 +41,7 @@ impl RevisionManager {
     }
 
     pub async fn load_document(&mut self) -> DocResult<RichTextDelta> {
-        let doc = self.cache.fetch_document().await?;
+        let doc = self.cache.load_document().await?;
         self.update_rev_id_counter_value(doc.rev_id);
         Ok(doc.delta()?)
     }

+ 1 - 0
frontend/rust-lib/flowy-document/src/services/doc/revision/sync.rs

@@ -53,6 +53,7 @@ impl RevisionUploadStream {
     }
 
     async fn send_next_revision(&self) -> DocResult<()> {
+        log::debug!("😁Tick");
         match self.revisions.next().await? {
             None => Ok(()),
             Some(record) => {

+ 1 - 0
frontend/rust-lib/flowy-document/tests/editor/mod.rs

@@ -1,6 +1,7 @@
 #![allow(clippy::module_inception)]
 mod attribute_test;
 mod op_test;
+mod revision_test;
 mod serde_test;
 mod undo_redo_test;
 

+ 8 - 0
frontend/rust-lib/flowy-document/tests/editor/revision_test.rs

@@ -0,0 +1,8 @@
+use flowy_test::editor::*;
+
+#[tokio::test]
+async fn create_doc() {
+    let test = EditorTest::new().await;
+    let _editor = test.create_doc().await;
+    println!("123");
+}

+ 22 - 0
frontend/rust-lib/flowy-test/src/editor.rs

@@ -0,0 +1,22 @@
+use crate::{helper::ViewTest, FlowySDKTest};
+use flowy_document::services::doc::edit::ClientDocEditor;
+use flowy_document_infra::entities::doc::DocIdentifier;
+use std::sync::Arc;
+
+pub struct EditorTest {
+    pub sdk: FlowySDKTest,
+}
+
+impl EditorTest {
+    pub async fn new() -> Self {
+        let sdk = FlowySDKTest::setup();
+        let _ = sdk.init_user().await;
+        Self { sdk }
+    }
+
+    pub async fn create_doc(&self) -> Arc<ClientDocEditor> {
+        let test = ViewTest::new(&self.sdk).await;
+        let doc_identifier: DocIdentifier = test.view.id.clone().into();
+        self.sdk.flowy_document.open(doc_identifier).await.unwrap()
+    }
+}

+ 14 - 17
frontend/rust-lib/flowy-test/src/builder.rs → frontend/rust-lib/flowy-test/src/event_builder.rs

@@ -5,37 +5,36 @@ use std::{
     hash::Hash,
 };
 
-use crate::FlowyTestSDK;
-use lib_dispatch::prelude::*;
-
+use crate::FlowySDKTest;
 use flowy_core::errors::WorkspaceError;
-use flowy_sdk::*;
+
 use flowy_user::errors::UserError;
+use lib_dispatch::prelude::*;
 use std::{convert::TryFrom, marker::PhantomData, sync::Arc};
 
-pub type FlowyWorkspaceTest = Builder<WorkspaceError>;
-impl FlowyWorkspaceTest {
-    pub fn new(sdk: FlowyTestSDK) -> Self { Builder::test(TestContext::new(sdk)) }
+pub type CoreModuleEventBuilder = EventBuilder<WorkspaceError>;
+impl CoreModuleEventBuilder {
+    pub fn new(sdk: FlowySDKTest) -> Self { EventBuilder::test(TestContext::new(sdk)) }
 }
 
-pub type UserTest = Builder<UserError>;
-impl UserTest {
-    pub fn new(sdk: FlowyTestSDK) -> Self { Builder::test(TestContext::new(sdk)) }
+pub type UserModuleEventBuilder = EventBuilder<UserError>;
+impl UserModuleEventBuilder {
+    pub fn new(sdk: FlowySDKTest) -> Self { EventBuilder::test(TestContext::new(sdk)) }
     pub fn user_profile(&self) -> &Option<UserProfile> { &self.user_profile }
 }
 
 #[derive(Clone)]
-pub struct Builder<E> {
+pub struct EventBuilder<E> {
     context: TestContext,
     user_profile: Option<UserProfile>,
     err_phantom: PhantomData<E>,
 }
 
-impl<E> Builder<E>
+impl<E> EventBuilder<E>
 where
     E: FromBytes + Debug,
 {
-    pub(crate) fn test(context: TestContext) -> Self {
+    fn test(context: TestContext) -> Self {
         Self {
             context,
             user_profile: None,
@@ -111,8 +110,6 @@ where
         self
     }
 
-    pub fn sdk(&self) -> FlowySDK { self.context.sdk.clone() }
-
     fn dispatch(&self) -> Arc<EventDispatcher> { self.context.sdk.dispatcher() }
 
     fn get_response(&self) -> EventResponse {
@@ -128,13 +125,13 @@ where
 
 #[derive(Clone)]
 pub struct TestContext {
-    sdk: FlowyTestSDK,
+    pub sdk: FlowySDKTest,
     request: Option<ModuleRequest>,
     response: Option<EventResponse>,
 }
 
 impl TestContext {
-    pub fn new(sdk: FlowyTestSDK) -> Self {
+    pub fn new(sdk: FlowySDKTest) -> Self {
         Self {
             sdk,
             request: None,

+ 269 - 6
frontend/rust-lib/flowy-test/src/helper.rs

@@ -1,19 +1,282 @@
+use crate::prelude::*;
 use bytes::Bytes;
-use lib_dispatch::prelude::{EventDispatcher, ModuleRequest, ToBytes};
-use lib_infra::{kv::KV, uuid};
-
 use flowy_core::{
-    entities::workspace::{CreateWorkspaceRequest, QueryWorkspaceRequest, Workspace},
-    errors::WorkspaceError,
-    event::WorkspaceEvent::{CreateWorkspace, OpenWorkspace},
+    entities::{
+        app::*,
+        trash::{RepeatedTrash, TrashIdentifier},
+        view::*,
+        workspace::{CreateWorkspaceRequest, QueryWorkspaceRequest, Workspace, *},
+    },
+    errors::{ErrorCode, WorkspaceError},
+    event::WorkspaceEvent::{CreateWorkspace, OpenWorkspace, *},
 };
+use flowy_document_infra::entities::doc::Doc;
 use flowy_user::{
     entities::{SignInRequest, SignUpRequest, UserProfile},
     errors::UserError,
     event::UserEvent::{SignIn, SignOut, SignUp},
 };
+use lib_dispatch::prelude::{EventDispatcher, ModuleRequest, ToBytes};
+use lib_infra::{kv::KV, uuid};
 use std::{fs, path::PathBuf, sync::Arc};
 
+pub struct WorkspaceTest {
+    pub sdk: FlowySDKTest,
+    pub workspace: Workspace,
+}
+
+impl WorkspaceTest {
+    pub async fn new() -> Self {
+        let sdk = FlowySDKTest::setup();
+        let _ = sdk.init_user().await;
+        let workspace = create_workspace(&sdk, "Workspace", "").await;
+        open_workspace(&sdk, &workspace.id).await;
+
+        Self { sdk, workspace }
+    }
+}
+
+pub struct AppTest {
+    pub sdk: FlowySDKTest,
+    pub workspace: Workspace,
+    pub app: App,
+}
+
+impl AppTest {
+    pub async fn new() -> Self {
+        let sdk = FlowySDKTest::setup();
+        let _ = sdk.init_user().await;
+        let workspace = create_workspace(&sdk, "Workspace", "").await;
+        open_workspace(&sdk, &workspace.id).await;
+        let app = create_app(&sdk, "App", "AppFlowy GitHub Project", &workspace.id).await;
+        Self { sdk, workspace, app }
+    }
+
+    pub async fn move_app_to_trash(&self) {
+        let request = UpdateAppRequest {
+            app_id: self.app.id.clone(),
+            name: None,
+            desc: None,
+            color_style: None,
+            is_trash: Some(true),
+        };
+        update_app(&self.sdk, request).await;
+    }
+}
+
+pub struct ViewTest {
+    pub sdk: FlowySDKTest,
+    pub workspace: Workspace,
+    pub app: App,
+    pub view: View,
+}
+
+impl ViewTest {
+    pub async fn new(sdk: &FlowySDKTest) -> Self {
+        let workspace = create_workspace(&sdk, "Workspace", "").await;
+        open_workspace(&sdk, &workspace.id).await;
+        let app = create_app(&sdk, "App", "AppFlowy GitHub Project", &workspace.id).await;
+        let view = create_view(&sdk, &app.id).await;
+        Self {
+            sdk: sdk.clone(),
+            workspace,
+            app,
+            view,
+        }
+    }
+
+    pub async fn delete_views(&self, view_ids: Vec<String>) {
+        let request = QueryViewRequest { view_ids };
+        delete_view(&self.sdk, request).await;
+    }
+
+    pub async fn delete_views_permanent(&self, view_ids: Vec<String>) {
+        let request = QueryViewRequest { view_ids };
+        delete_view(&self.sdk, request).await;
+
+        CoreModuleEventBuilder::new(self.sdk.clone())
+            .event(DeleteAll)
+            .async_send()
+            .await;
+    }
+}
+
+pub fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> {
+    vec![
+        ("".to_owned(), ErrorCode::WorkspaceNameInvalid),
+        ("1234".repeat(100), ErrorCode::WorkspaceNameTooLong),
+    ]
+}
+
+pub async fn create_workspace(sdk: &FlowySDKTest, name: &str, desc: &str) -> Workspace {
+    let request = CreateWorkspaceRequest {
+        name: name.to_owned(),
+        desc: desc.to_owned(),
+    };
+
+    let workspace = CoreModuleEventBuilder::new(sdk.clone())
+        .event(CreateWorkspace)
+        .request(request)
+        .async_send()
+        .await
+        .parse::<Workspace>();
+    workspace
+}
+
+async fn open_workspace(sdk: &FlowySDKTest, workspace_id: &str) {
+    let request = QueryWorkspaceRequest {
+        workspace_id: Some(workspace_id.to_owned()),
+    };
+    let _ = CoreModuleEventBuilder::new(sdk.clone())
+        .event(OpenWorkspace)
+        .request(request)
+        .async_send()
+        .await;
+}
+
+pub async fn read_workspace(sdk: &FlowySDKTest, request: QueryWorkspaceRequest) -> Vec<Workspace> {
+    let repeated_workspace = CoreModuleEventBuilder::new(sdk.clone())
+        .event(ReadWorkspaces)
+        .request(request.clone())
+        .async_send()
+        .await
+        .parse::<RepeatedWorkspace>();
+
+    let workspaces;
+    if let Some(workspace_id) = &request.workspace_id {
+        workspaces = repeated_workspace
+            .into_inner()
+            .into_iter()
+            .filter(|workspace| &workspace.id == workspace_id)
+            .collect::<Vec<Workspace>>();
+        debug_assert_eq!(workspaces.len(), 1);
+    } else {
+        workspaces = repeated_workspace.items;
+    }
+
+    workspaces
+}
+
+pub async fn create_app(sdk: &FlowySDKTest, name: &str, desc: &str, workspace_id: &str) -> App {
+    let create_app_request = CreateAppRequest {
+        workspace_id: workspace_id.to_owned(),
+        name: name.to_string(),
+        desc: desc.to_string(),
+        color_style: Default::default(),
+    };
+
+    let app = CoreModuleEventBuilder::new(sdk.clone())
+        .event(CreateApp)
+        .request(create_app_request)
+        .async_send()
+        .await
+        .parse::<App>();
+    app
+}
+
+pub async fn delete_app(sdk: &FlowySDKTest, app_id: &str) {
+    let delete_app_request = AppIdentifier {
+        app_id: app_id.to_string(),
+    };
+
+    CoreModuleEventBuilder::new(sdk.clone())
+        .event(DeleteApp)
+        .request(delete_app_request)
+        .async_send()
+        .await;
+}
+
+pub async fn update_app(sdk: &FlowySDKTest, request: UpdateAppRequest) {
+    CoreModuleEventBuilder::new(sdk.clone())
+        .event(UpdateApp)
+        .request(request)
+        .async_send()
+        .await;
+}
+
+pub async fn read_app(sdk: &FlowySDKTest, request: QueryAppRequest) -> App {
+    let app = CoreModuleEventBuilder::new(sdk.clone())
+        .event(ReadApp)
+        .request(request)
+        .async_send()
+        .await
+        .parse::<App>();
+
+    app
+}
+
+pub async fn create_view_with_request(sdk: &FlowySDKTest, request: CreateViewRequest) -> View {
+    let view = CoreModuleEventBuilder::new(sdk.clone())
+        .event(CreateView)
+        .request(request)
+        .async_send()
+        .await
+        .parse::<View>();
+    view
+}
+
+pub async fn create_view(sdk: &FlowySDKTest, app_id: &str) -> View {
+    let request = CreateViewRequest {
+        belong_to_id: app_id.to_string(),
+        name: "View A".to_string(),
+        desc: "".to_string(),
+        thumbnail: Some("http://1.png".to_string()),
+        view_type: ViewType::Doc,
+    };
+
+    create_view_with_request(sdk, request).await
+}
+
+pub async fn update_view(sdk: &FlowySDKTest, request: UpdateViewRequest) {
+    CoreModuleEventBuilder::new(sdk.clone())
+        .event(UpdateView)
+        .request(request)
+        .async_send()
+        .await;
+}
+
+pub async fn read_view(sdk: &FlowySDKTest, request: QueryViewRequest) -> View {
+    CoreModuleEventBuilder::new(sdk.clone())
+        .event(ReadView)
+        .request(request)
+        .async_send()
+        .await
+        .parse::<View>()
+}
+
+pub async fn delete_view(sdk: &FlowySDKTest, request: QueryViewRequest) {
+    CoreModuleEventBuilder::new(sdk.clone())
+        .event(DeleteView)
+        .request(request)
+        .async_send()
+        .await;
+}
+
+pub async fn read_trash(sdk: &FlowySDKTest) -> RepeatedTrash {
+    CoreModuleEventBuilder::new(sdk.clone())
+        .event(ReadTrash)
+        .async_send()
+        .await
+        .parse::<RepeatedTrash>()
+}
+
+pub async fn putback_trash(sdk: &FlowySDKTest, id: TrashIdentifier) {
+    CoreModuleEventBuilder::new(sdk.clone())
+        .event(PutbackTrash)
+        .request(id)
+        .async_send()
+        .await;
+}
+
+pub async fn open_view(sdk: &FlowySDKTest, request: QueryViewRequest) -> Doc {
+    CoreModuleEventBuilder::new(sdk.clone())
+        .event(OpenView)
+        .request(request)
+        .async_send()
+        .await
+        .parse::<Doc>()
+}
+
 pub fn root_dir() -> String {
     // https://doc.rust-lang.org/cargo/reference/environment-variables.html
     let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| "./".to_owned());

+ 22 - 22
frontend/rust-lib/flowy-test/src/lib.rs

@@ -1,6 +1,6 @@
-pub mod builder;
-mod helper;
-pub mod workspace;
+pub mod editor;
+pub mod event_builder;
+pub mod helper;
 
 use crate::helper::*;
 use backend_service::configuration::{get_client_server_configuration, ClientServerConfiguration};
@@ -9,40 +9,40 @@ use flowy_user::entities::UserProfile;
 use lib_infra::uuid;
 
 pub mod prelude {
-    pub use crate::{builder::*, helper::*, *};
+    pub use crate::{event_builder::*, helper::*, *};
     pub use lib_dispatch::prelude::*;
 }
 
-pub type FlowyTestSDK = FlowySDK;
-
 #[derive(Clone)]
-pub struct FlowyTest {
-    pub sdk: FlowyTestSDK,
+pub struct FlowySDKTest(pub FlowySDK);
+
+impl std::ops::Deref for FlowySDKTest {
+    type Target = FlowySDK;
+
+    fn deref(&self) -> &Self::Target { &self.0 }
 }
 
-impl FlowyTest {
+impl FlowySDKTest {
     pub fn setup() -> Self {
         let server_config = get_client_server_configuration().unwrap();
-        let test = Self::setup_with(server_config);
-        std::mem::forget(test.sdk.dispatcher());
-        test
+        let sdk = Self::setup_with(server_config);
+        std::mem::forget(sdk.dispatcher());
+        sdk
+    }
+
+    pub fn setup_with(server_config: ClientServerConfiguration) -> Self {
+        let config = FlowySDKConfig::new(&root_dir(), server_config, &uuid()).log_filter("debug");
+        let sdk = FlowySDK::new(config);
+        Self(sdk)
     }
 
     pub async fn sign_up(&self) -> SignUpContext {
-        let context = async_sign_up(self.sdk.dispatcher()).await;
+        let context = async_sign_up(self.0.dispatcher()).await;
         context
     }
 
     pub async fn init_user(&self) -> UserProfile {
-        let context = async_sign_up(self.sdk.dispatcher()).await;
+        let context = async_sign_up(self.0.dispatcher()).await;
         context.user_profile
     }
-
-    pub fn setup_with(server_config: ClientServerConfiguration) -> Self {
-        let config = FlowySDKConfig::new(&root_dir(), server_config, &uuid()).log_filter("debug");
-        let sdk = FlowySDK::new(config);
-        Self { sdk }
-    }
-
-    pub fn sdk(&self) -> FlowyTestSDK { self.sdk.clone() }
 }

+ 0 - 276
frontend/rust-lib/flowy-test/src/workspace.rs

@@ -1,276 +0,0 @@
-use crate::prelude::*;
-use flowy_core::{
-    entities::{
-        app::*,
-        trash::{RepeatedTrash, TrashIdentifier},
-        view::*,
-        workspace::*,
-    },
-    errors::ErrorCode,
-    event::WorkspaceEvent::*,
-};
-use flowy_document_infra::entities::doc::Doc;
-
-pub struct WorkspaceTest {
-    pub sdk: FlowyTestSDK,
-    pub workspace: Workspace,
-}
-
-impl WorkspaceTest {
-    pub async fn new() -> Self {
-        let test = FlowyTest::setup();
-        let _ = test.init_user().await;
-        let workspace = create_workspace(&test.sdk, "Workspace", "").await;
-        open_workspace(&test.sdk, &workspace.id).await;
-
-        Self {
-            sdk: test.sdk,
-            workspace,
-        }
-    }
-}
-
-pub struct AppTest {
-    pub sdk: FlowyTestSDK,
-    pub workspace: Workspace,
-    pub app: App,
-}
-
-impl AppTest {
-    pub async fn new() -> Self {
-        let test = FlowyTest::setup();
-        let _ = test.init_user().await;
-        let workspace = create_workspace(&test.sdk, "Workspace", "").await;
-        open_workspace(&test.sdk, &workspace.id).await;
-        let app = create_app(&test.sdk, "App", "AppFlowy GitHub Project", &workspace.id).await;
-        Self {
-            sdk: test.sdk,
-            workspace,
-            app,
-        }
-    }
-
-    pub async fn move_app_to_trash(&self) {
-        let request = UpdateAppRequest {
-            app_id: self.app.id.clone(),
-            name: None,
-            desc: None,
-            color_style: None,
-            is_trash: Some(true),
-        };
-        update_app(&self.sdk, request).await;
-    }
-}
-
-pub struct ViewTest {
-    pub sdk: FlowyTestSDK,
-    pub workspace: Workspace,
-    pub app: App,
-    pub view: View,
-}
-
-impl ViewTest {
-    pub async fn new(test: &FlowyTest) -> Self {
-        let workspace = create_workspace(&test.sdk, "Workspace", "").await;
-        open_workspace(&test.sdk, &workspace.id).await;
-        let app = create_app(&test.sdk, "App", "AppFlowy GitHub Project", &workspace.id).await;
-        let view = create_view(&test.sdk, &app.id).await;
-        Self {
-            sdk: test.sdk.clone(),
-            workspace,
-            app,
-            view,
-        }
-    }
-
-    pub async fn delete_views(&self, view_ids: Vec<String>) {
-        let request = QueryViewRequest { view_ids };
-        delete_view(&self.sdk, request).await;
-    }
-
-    pub async fn delete_views_permanent(&self, view_ids: Vec<String>) {
-        let request = QueryViewRequest { view_ids };
-        delete_view(&self.sdk, request).await;
-
-        FlowyWorkspaceTest::new(self.sdk.clone())
-            .event(DeleteAll)
-            .async_send()
-            .await;
-    }
-}
-
-pub fn invalid_workspace_name_test_case() -> Vec<(String, ErrorCode)> {
-    vec![
-        ("".to_owned(), ErrorCode::WorkspaceNameInvalid),
-        ("1234".repeat(100), ErrorCode::WorkspaceNameTooLong),
-    ]
-}
-
-pub async fn create_workspace(sdk: &FlowyTestSDK, name: &str, desc: &str) -> Workspace {
-    let request = CreateWorkspaceRequest {
-        name: name.to_owned(),
-        desc: desc.to_owned(),
-    };
-
-    let workspace = FlowyWorkspaceTest::new(sdk.clone())
-        .event(CreateWorkspace)
-        .request(request)
-        .async_send()
-        .await
-        .parse::<Workspace>();
-    workspace
-}
-
-async fn open_workspace(sdk: &FlowyTestSDK, workspace_id: &str) {
-    let request = QueryWorkspaceRequest {
-        workspace_id: Some(workspace_id.to_owned()),
-    };
-    let _ = FlowyWorkspaceTest::new(sdk.clone())
-        .event(OpenWorkspace)
-        .request(request)
-        .async_send()
-        .await;
-}
-
-pub async fn read_workspace(sdk: &FlowyTestSDK, request: QueryWorkspaceRequest) -> Vec<Workspace> {
-    let repeated_workspace = FlowyWorkspaceTest::new(sdk.clone())
-        .event(ReadWorkspaces)
-        .request(request.clone())
-        .async_send()
-        .await
-        .parse::<RepeatedWorkspace>();
-
-    let workspaces;
-    if let Some(workspace_id) = &request.workspace_id {
-        workspaces = repeated_workspace
-            .into_inner()
-            .into_iter()
-            .filter(|workspace| &workspace.id == workspace_id)
-            .collect::<Vec<Workspace>>();
-        debug_assert_eq!(workspaces.len(), 1);
-    } else {
-        workspaces = repeated_workspace.items;
-    }
-
-    workspaces
-}
-
-pub async fn create_app(sdk: &FlowyTestSDK, name: &str, desc: &str, workspace_id: &str) -> App {
-    let create_app_request = CreateAppRequest {
-        workspace_id: workspace_id.to_owned(),
-        name: name.to_string(),
-        desc: desc.to_string(),
-        color_style: Default::default(),
-    };
-
-    let app = FlowyWorkspaceTest::new(sdk.clone())
-        .event(CreateApp)
-        .request(create_app_request)
-        .async_send()
-        .await
-        .parse::<App>();
-    app
-}
-
-pub async fn delete_app(sdk: &FlowyTestSDK, app_id: &str) {
-    let delete_app_request = AppIdentifier {
-        app_id: app_id.to_string(),
-    };
-
-    FlowyWorkspaceTest::new(sdk.clone())
-        .event(DeleteApp)
-        .request(delete_app_request)
-        .async_send()
-        .await;
-}
-
-pub async fn update_app(sdk: &FlowyTestSDK, request: UpdateAppRequest) {
-    FlowyWorkspaceTest::new(sdk.clone())
-        .event(UpdateApp)
-        .request(request)
-        .async_send()
-        .await;
-}
-
-pub async fn read_app(sdk: &FlowyTestSDK, request: QueryAppRequest) -> App {
-    let app = FlowyWorkspaceTest::new(sdk.clone())
-        .event(ReadApp)
-        .request(request)
-        .async_send()
-        .await
-        .parse::<App>();
-
-    app
-}
-
-pub async fn create_view_with_request(sdk: &FlowyTestSDK, request: CreateViewRequest) -> View {
-    let view = FlowyWorkspaceTest::new(sdk.clone())
-        .event(CreateView)
-        .request(request)
-        .async_send()
-        .await
-        .parse::<View>();
-    view
-}
-
-pub async fn create_view(sdk: &FlowyTestSDK, app_id: &str) -> View {
-    let request = CreateViewRequest {
-        belong_to_id: app_id.to_string(),
-        name: "View A".to_string(),
-        desc: "".to_string(),
-        thumbnail: Some("http://1.png".to_string()),
-        view_type: ViewType::Doc,
-    };
-
-    create_view_with_request(sdk, request).await
-}
-
-pub async fn update_view(sdk: &FlowyTestSDK, request: UpdateViewRequest) {
-    FlowyWorkspaceTest::new(sdk.clone())
-        .event(UpdateView)
-        .request(request)
-        .async_send()
-        .await;
-}
-
-pub async fn read_view(sdk: &FlowyTestSDK, request: QueryViewRequest) -> View {
-    FlowyWorkspaceTest::new(sdk.clone())
-        .event(ReadView)
-        .request(request)
-        .async_send()
-        .await
-        .parse::<View>()
-}
-
-pub async fn delete_view(sdk: &FlowyTestSDK, request: QueryViewRequest) {
-    FlowyWorkspaceTest::new(sdk.clone())
-        .event(DeleteView)
-        .request(request)
-        .async_send()
-        .await;
-}
-
-pub async fn read_trash(sdk: &FlowyTestSDK) -> RepeatedTrash {
-    FlowyWorkspaceTest::new(sdk.clone())
-        .event(ReadTrash)
-        .async_send()
-        .await
-        .parse::<RepeatedTrash>()
-}
-
-pub async fn putback_trash(sdk: &FlowyTestSDK, id: TrashIdentifier) {
-    FlowyWorkspaceTest::new(sdk.clone())
-        .event(PutbackTrash)
-        .request(id)
-        .async_send()
-        .await;
-}
-
-pub async fn open_view(sdk: &FlowyTestSDK, request: QueryViewRequest) -> Doc {
-    FlowyWorkspaceTest::new(sdk.clone())
-        .event(OpenView)
-        .request(request)
-        .async_send()
-        .await
-        .parse::<Doc>()
-}

+ 12 - 12
frontend/rust-lib/flowy-user/tests/event/auth_test.rs

@@ -1,11 +1,11 @@
 use crate::helper::*;
-use flowy_test::{builder::UserTest, FlowyTest};
+use flowy_test::{event_builder::UserModuleEventBuilder, FlowySDKTest};
 use flowy_user::{errors::ErrorCode, event::UserEvent::*, prelude::*};
 
 #[tokio::test]
 async fn sign_up_with_invalid_email() {
     for email in invalid_email_test_case() {
-        let test = FlowyTest::setup();
+        let sdk = FlowySDKTest::setup();
         let request = SignUpRequest {
             email: email.to_string(),
             name: valid_name(),
@@ -13,7 +13,7 @@ async fn sign_up_with_invalid_email() {
         };
 
         assert_eq!(
-            UserTest::new(test.sdk)
+            UserModuleEventBuilder::new(sdk)
                 .event(SignUp)
                 .request(request)
                 .async_send()
@@ -27,14 +27,14 @@ async fn sign_up_with_invalid_email() {
 #[tokio::test]
 async fn sign_up_with_invalid_password() {
     for password in invalid_password_test_case() {
-        let test = FlowyTest::setup();
+        let sdk = FlowySDKTest::setup();
         let request = SignUpRequest {
             email: random_email(),
             name: valid_name(),
             password,
         };
 
-        UserTest::new(test.sdk)
+        UserModuleEventBuilder::new(sdk)
             .event(SignUp)
             .request(request)
             .async_send()
@@ -45,8 +45,8 @@ async fn sign_up_with_invalid_password() {
 
 #[tokio::test]
 async fn sign_in_success() {
-    let test = FlowyTest::setup();
-    let _ = UserTest::new(test.sdk()).event(SignOut).sync_send();
+    let test = FlowySDKTest::setup();
+    let _ = UserModuleEventBuilder::new(test.clone()).event(SignOut).sync_send();
     let sign_up_context = test.sign_up().await;
 
     let request = SignInRequest {
@@ -55,7 +55,7 @@ async fn sign_in_success() {
         name: "".to_string(),
     };
 
-    let response = UserTest::new(test.sdk())
+    let response = UserModuleEventBuilder::new(test.clone())
         .event(SignIn)
         .request(request)
         .async_send()
@@ -67,7 +67,7 @@ async fn sign_in_success() {
 #[tokio::test]
 async fn sign_in_with_invalid_email() {
     for email in invalid_email_test_case() {
-        let test = FlowyTest::setup();
+        let sdk = FlowySDKTest::setup();
         let request = SignInRequest {
             email: email.to_string(),
             password: login_password(),
@@ -75,7 +75,7 @@ async fn sign_in_with_invalid_email() {
         };
 
         assert_eq!(
-            UserTest::new(test.sdk)
+            UserModuleEventBuilder::new(sdk)
                 .event(SignIn)
                 .request(request)
                 .async_send()
@@ -90,7 +90,7 @@ async fn sign_in_with_invalid_email() {
 #[tokio::test]
 async fn sign_in_with_invalid_password() {
     for password in invalid_password_test_case() {
-        let test = FlowyTest::setup();
+        let sdk = FlowySDKTest::setup();
 
         let request = SignInRequest {
             email: random_email(),
@@ -98,7 +98,7 @@ async fn sign_in_with_invalid_password() {
             name: "".to_string(),
         };
 
-        UserTest::new(test.sdk)
+        UserModuleEventBuilder::new(sdk)
             .event(SignIn)
             .request(request)
             .async_send()

+ 1 - 1
frontend/rust-lib/flowy-user/tests/event/helper.rs

@@ -1,5 +1,5 @@
 pub use flowy_test::{
-    builder::*,
+    event_builder::*,
     prelude::{login_password, random_email},
 };
 

+ 28 - 22
frontend/rust-lib/flowy-user/tests/event/user_profile_test.rs

@@ -1,13 +1,13 @@
 use crate::helper::*;
-use flowy_test::{builder::UserTest, FlowyTest};
+use flowy_test::{event_builder::UserModuleEventBuilder, FlowySDKTest};
 use flowy_user::{errors::ErrorCode, event::UserEvent::*, prelude::*};
 use lib_infra::uuid;
 use serial_test::*;
 
 #[tokio::test]
 async fn user_profile_get_failed() {
-    let test = FlowyTest::setup();
-    let result = UserTest::new(test.sdk)
+    let sdk = FlowySDKTest::setup();
+    let result = UserModuleEventBuilder::new(sdk)
         .event(GetUserProfile)
         .assert_error()
         .async_send()
@@ -18,9 +18,9 @@ async fn user_profile_get_failed() {
 #[tokio::test]
 #[serial]
 async fn user_profile_get() {
-    let test = FlowyTest::setup();
+    let test = FlowySDKTest::setup();
     let user_profile = test.init_user().await;
-    let user = UserTest::new(test.sdk.clone())
+    let user = UserModuleEventBuilder::new(test.clone())
         .event(GetUserProfile)
         .sync_send()
         .parse::<UserProfile>();
@@ -30,13 +30,16 @@ async fn user_profile_get() {
 #[tokio::test]
 #[serial]
 async fn user_update_with_name() {
-    let test = FlowyTest::setup();
-    let user = test.init_user().await;
+    let sdk = FlowySDKTest::setup();
+    let user = sdk.init_user().await;
     let new_name = "hello_world".to_owned();
     let request = UpdateUserRequest::new(&user.id).name(&new_name);
-    let _ = UserTest::new(test.sdk()).event(UpdateUser).request(request).sync_send();
+    let _ = UserModuleEventBuilder::new(sdk.clone())
+        .event(UpdateUser)
+        .request(request)
+        .sync_send();
 
-    let user_profile = UserTest::new(test.sdk())
+    let user_profile = UserModuleEventBuilder::new(sdk.clone())
         .event(GetUserProfile)
         .assert_error()
         .sync_send()
@@ -48,12 +51,15 @@ async fn user_update_with_name() {
 #[tokio::test]
 #[serial]
 async fn user_update_with_email() {
-    let test = FlowyTest::setup();
-    let user = test.init_user().await;
+    let sdk = FlowySDKTest::setup();
+    let user = sdk.init_user().await;
     let new_email = format!("{}@gmail.com", uuid());
     let request = UpdateUserRequest::new(&user.id).email(&new_email);
-    let _ = UserTest::new(test.sdk()).event(UpdateUser).request(request).sync_send();
-    let user_profile = UserTest::new(test.sdk())
+    let _ = UserModuleEventBuilder::new(sdk.clone())
+        .event(UpdateUser)
+        .request(request)
+        .sync_send();
+    let user_profile = UserModuleEventBuilder::new(sdk.clone())
         .event(GetUserProfile)
         .assert_error()
         .sync_send()
@@ -65,12 +71,12 @@ async fn user_update_with_email() {
 #[tokio::test]
 #[serial]
 async fn user_update_with_password() {
-    let test = FlowyTest::setup();
-    let user = test.init_user().await;
+    let sdk = FlowySDKTest::setup();
+    let user = sdk.init_user().await;
     let new_password = "H123world!".to_owned();
     let request = UpdateUserRequest::new(&user.id).password(&new_password);
 
-    let _ = UserTest::new(test.sdk())
+    let _ = UserModuleEventBuilder::new(sdk.clone())
         .event(UpdateUser)
         .request(request)
         .sync_send()
@@ -80,12 +86,12 @@ async fn user_update_with_password() {
 #[tokio::test]
 #[serial]
 async fn user_update_with_invalid_email() {
-    let test = FlowyTest::setup();
+    let test = FlowySDKTest::setup();
     let user = test.init_user().await;
     for email in invalid_email_test_case() {
         let request = UpdateUserRequest::new(&user.id).email(&email);
         assert_eq!(
-            UserTest::new(test.sdk())
+            UserModuleEventBuilder::new(test.clone())
                 .event(UpdateUser)
                 .request(request)
                 .sync_send()
@@ -99,12 +105,12 @@ async fn user_update_with_invalid_email() {
 #[tokio::test]
 #[serial]
 async fn user_update_with_invalid_password() {
-    let test = FlowyTest::setup();
+    let test = FlowySDKTest::setup();
     let user = test.init_user().await;
     for password in invalid_password_test_case() {
         let request = UpdateUserRequest::new(&user.id).password(&password);
 
-        UserTest::new(test.sdk())
+        UserModuleEventBuilder::new(test.clone())
             .event(UpdateUser)
             .request(request)
             .sync_send()
@@ -115,10 +121,10 @@ async fn user_update_with_invalid_password() {
 #[tokio::test]
 #[serial]
 async fn user_update_with_invalid_name() {
-    let test = FlowyTest::setup();
+    let test = FlowySDKTest::setup();
     let user = test.init_user().await;
     let request = UpdateUserRequest::new(&user.id).name("");
-    UserTest::new(test.sdk())
+    UserModuleEventBuilder::new(test.clone())
         .event(UpdateUser)
         .request(request)
         .sync_send()

+ 3 - 0
shared-lib/flowy-document-infra/src/entities/ws/ws.rs

@@ -6,8 +6,11 @@ use std::convert::{TryFrom, TryInto};
 
 #[derive(Debug, Clone, ProtoBuf_Enum, Eq, PartialEq, Hash)]
 pub enum WsDataType {
+    // The frontend receives the Acked means the backend has accepted the revision
     Acked      = 0,
+    // The frontend receives the PushRev event means the backend is pushing the new revision to frontend
     PushRev    = 1,
+    // The fronted receives the PullRev event means the backend try to pull the revision from frontend
     PullRev    = 2, // data should be Revision
     Conflict   = 3,
     NewDocUser = 4,

+ 2 - 0
shared-lib/lib-ot/src/errors.rs

@@ -36,6 +36,7 @@ impl OTError {
     }
 
     static_ot_error!(duplicate_revision, OTErrorCode::DuplicatedRevision);
+    static_ot_error!(revision_id_conflict, OTErrorCode::RevisionIDConflict);
 }
 
 impl fmt::Display for OTError {
@@ -66,6 +67,7 @@ pub enum OTErrorCode {
     RedoFail,
     SerdeError,
     DuplicatedRevision,
+    RevisionIDConflict,
 }
 
 pub struct ErrorBuilder {

+ 6 - 20
shared-lib/lib-ot/src/revision/cache.rs

@@ -33,8 +33,12 @@ impl RevisionMemoryCache {
     pub fn new() -> Self { RevisionMemoryCache::default() }
 
     pub async fn add_revision(&self, revision: Revision) -> Result<(), OTError> {
-        if self.revs_map.contains_key(&revision.rev_id) {
-            return Ok(());
+        // The last revision's rev_id must be greater than the new one.
+        if let Some(rev_id) = self.pending_revs.read().await.back() {
+            if *rev_id >= revision.rev_id {
+                return Err(OTError::revision_id_conflict()
+                    .context(format!("The new revision's id must be greater than {}", rev_id)));
+            }
         }
 
         self.pending_revs.write().await.push_back(revision.rev_id);
@@ -121,21 +125,3 @@ impl RevisionRecord {
 
     pub fn ack(&mut self) { self.state = RevState::Acked; }
 }
-
-pub struct PendingRevId {
-    pub rev_id: i64,
-    pub sender: RevIdSender,
-}
-
-impl PendingRevId {
-    pub fn new(rev_id: i64, sender: RevIdSender) -> Self { Self { rev_id, sender } }
-
-    pub fn finish(&self, rev_id: i64) -> bool {
-        if self.rev_id > rev_id {
-            false
-        } else {
-            let _ = self.sender.send(self.rev_id);
-            true
-        }
-    }
-}

+ 2 - 0
shared-lib/lib-ot/src/revision/model.rs

@@ -25,6 +25,8 @@ pub struct Revision {
 
 impl Revision {
     pub fn is_empty(&self) -> bool { self.base_rev_id == self.rev_id }
+
+    pub fn pair_rev_id(&self) -> (i64, i64) { (self.base_rev_id, self.rev_id) }
 }
 
 impl std::fmt::Debug for Revision {