refactor tests

This commit is contained in:
appflowy 2021-12-08 17:33:22 +08:00
parent 4450d4410b
commit 7ac55f29db
30 changed files with 467 additions and 454 deletions

1
backend/Cargo.lock generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -46,7 +46,7 @@ impl DocError {
static_doc_error!(ws, ErrorCode::WsConnectError); static_doc_error!(ws, ErrorCode::WsConnectError);
static_doc_error!(internal, ErrorCode::InternalError); static_doc_error!(internal, ErrorCode::InternalError);
static_doc_error!(unauthorized, ErrorCode::UserUnauthorized); 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); 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 { impl std::convert::From<flowy_database::Error> for DocError {
fn from(error: flowy_database::Error) -> Self { fn from(error: flowy_database::Error) -> Self {
match error { 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), _ => DocError::internal().context(error),
} }
} }

View File

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

View File

@ -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") }

View File

@ -122,7 +122,7 @@ struct RevisionServerImpl {
impl RevisionServer for RevisionServerImpl { impl RevisionServer for RevisionServerImpl {
#[tracing::instrument(level = "debug", skip(self))] #[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 { let params = DocIdentifier {
doc_id: doc_id.to_string(), doc_id: doc_id.to_string(),
}; };
@ -131,7 +131,7 @@ impl RevisionServer for RevisionServerImpl {
ResultFuture::new(async move { ResultFuture::new(async move {
match server.read_doc(&token, params).await? { 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), Some(doc) => Ok(doc),
} }
}) })

View File

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

View File

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

View File

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

View File

@ -22,7 +22,7 @@ use lib_ot::{
}; };
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use tokio::{ use tokio::{
sync::RwLock, sync::{mpsc, RwLock},
task::{spawn_blocking, JoinHandle}, task::{spawn_blocking, JoinHandle},
}; };
@ -107,13 +107,15 @@ impl RevisionCache {
} }
} }
pub async fn fetch_document(&self) -> DocResult<Doc> { pub async fn load_document(&self) -> DocResult<Doc> {
let result = fetch_from_local(&self.doc_id, self.dish_cache.clone()).await; // 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() { if result.is_ok() {
return result; 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 delta_data = doc.data.as_bytes();
let revision = Revision::new( let revision = Revision::new(
doc.base_rev_id, 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(); 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)?; let revisions = disk_cache.read_revisions(&doc_id)?;
if revisions.is_empty() { 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 (base_rev_id, rev_id) = revisions.last().unwrap().pair_rev_id();
let rev_id: RevId = revisions.last().unwrap().rev_id.into();
let mut delta = RichTextDelta::new(); let mut delta = RichTextDelta::new();
for (_, revision) in revisions.into_iter().enumerate() { 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) => { Ok(local_delta) => {
delta = delta.compose(&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) => { Err(e) => {
log::error!("Deserialize delta from revision failed: {}", 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)] correct_delta_if_need(&mut delta);
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()))
}
},
}
Result::<Doc, DocError>::Ok(Doc { Result::<Doc, DocError>::Ok(Doc {
id: doc_id, id: doc_id,
data: delta.to_json(), data: delta.to_json(),
rev_id: rev_id.into(), rev_id,
base_rev_id: base_rev_id.into(), base_rev_id,
}) })
}) })
.await .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 correct_delta_if_need(delta: &mut RichTextDelta) {
fn validate_delta(doc_id: &str, disk_cache: Arc<DocRevisionDeskCache>, delta: &RichTextDelta) {
if delta.ops.last().is_none() { if delta.ops.last().is_none() {
return; return;
} }
let data = delta.ops.last().as_ref().unwrap().get_data(); let data = delta.ops.last().as_ref().unwrap().get_data();
if !data.ends_with('\n') { if !data.ends_with('\n') {
log::error!("The op must end with newline"); log::error!("The op must end with newline. Correcting it by inserting newline op");
let result = || { delta.ops.push(Operation::Insert("\n".into()));
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),
}
} }
} }

View File

@ -14,7 +14,7 @@ use std::sync::Arc;
use tokio::sync::mpsc; use tokio::sync::mpsc;
pub trait RevisionServer: Send + Sync { 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 { pub struct RevisionManager {
@ -41,7 +41,7 @@ impl RevisionManager {
} }
pub async fn load_document(&mut self) -> DocResult<RichTextDelta> { 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); self.update_rev_id_counter_value(doc.rev_id);
Ok(doc.delta()?) Ok(doc.delta()?)
} }

View File

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

View File

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

View File

@ -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");
}

View File

@ -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()
}
}

View File

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

View File

@ -1,19 +1,282 @@
use crate::prelude::*;
use bytes::Bytes; use bytes::Bytes;
use lib_dispatch::prelude::{EventDispatcher, ModuleRequest, ToBytes};
use lib_infra::{kv::KV, uuid};
use flowy_core::{ use flowy_core::{
entities::workspace::{CreateWorkspaceRequest, QueryWorkspaceRequest, Workspace}, entities::{
errors::WorkspaceError, app::*,
event::WorkspaceEvent::{CreateWorkspace, OpenWorkspace}, 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::{ use flowy_user::{
entities::{SignInRequest, SignUpRequest, UserProfile}, entities::{SignInRequest, SignUpRequest, UserProfile},
errors::UserError, errors::UserError,
event::UserEvent::{SignIn, SignOut, SignUp}, 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}; 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 { pub fn root_dir() -> String {
// https://doc.rust-lang.org/cargo/reference/environment-variables.html // https://doc.rust-lang.org/cargo/reference/environment-variables.html
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| "./".to_owned()); let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| "./".to_owned());

View File

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

View File

@ -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>()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,8 +33,12 @@ impl RevisionMemoryCache {
pub fn new() -> Self { RevisionMemoryCache::default() } pub fn new() -> Self { RevisionMemoryCache::default() }
pub async fn add_revision(&self, revision: Revision) -> Result<(), OTError> { pub async fn add_revision(&self, revision: Revision) -> Result<(), OTError> {
if self.revs_map.contains_key(&revision.rev_id) { // The last revision's rev_id must be greater than the new one.
return Ok(()); 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); 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 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
}
}
}

View File

@ -25,6 +25,8 @@ pub struct Revision {
impl Revision { impl Revision {
pub fn is_empty(&self) -> bool { self.base_rev_id == self.rev_id } 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 { impl std::fmt::Debug for Revision {