add flowy-test crate

This commit is contained in:
appflowy 2021-07-06 14:14:47 +08:00
parent 2fca817136
commit dfc2cbff4f
29 changed files with 383 additions and 227 deletions

View File

@ -14,6 +14,8 @@
<sourceFolder url="file://$MODULE_DIR$/rust-lib/flowy-sdk/tests" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/rust-lib/flowy-protobuf/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/scripts/flowy-tool/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/rust-lib/flowy-test/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/rust-lib/flowy-user/tests" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/app_flowy/packages/af_protobuf/.pub" />
<excludeFolder url="file://$MODULE_DIR$/app_flowy/packages/af_protobuf/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/app_flowy/packages/af_protobuf/build" />

1
rust-lib/.gitignore vendored
View File

@ -9,4 +9,5 @@ Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
**/**/*.log*
**/**/temp
bin/

View File

@ -7,6 +7,7 @@ members = [
"flowy-user",
"flowy-ast",
"flowy-derive",
"flowy-test",
]
[profile.dev]

View File

@ -93,7 +93,7 @@ impl std::convert::From<FFIRequest> for DispatchRequest {
} else {
Payload::None
};
let request = DispatchRequest::new(ffi_request.event, payload);
let request = DispatchRequest::new(ffi_request.event).payload(payload);
request
}
}

View File

@ -1,5 +1,4 @@
// https://docs.rs/syn/1.0.48/syn/struct.DeriveInput.html
#![feature(str_split_once)]
extern crate proc_macro;
use proc_macro::TokenStream;

View File

@ -1,79 +0,0 @@
pub use flowy_sdk::*;
use flowy_sys::prelude::*;
use std::{
fmt::{Debug, Display},
fs,
hash::Hash,
sync::Once,
};
static INIT: Once = Once::new();
pub fn init_sdk() {
let root_dir = root_dir();
INIT.call_once(|| {
FlowySDK::init_log(&root_dir);
});
FlowySDK::init(&root_dir);
}
fn root_dir() -> String {
let mut path = fs::canonicalize(".").unwrap();
path.push("tests/temp/flowy/");
let path_str = path.to_str().unwrap().to_string();
if !std::path::Path::new(&path).exists() {
std::fs::create_dir_all(path).unwrap();
}
path_str
}
pub struct EventTester {
request: DispatchRequest,
}
impl EventTester {
pub fn new<E, P>(event: E, payload: P) -> Self
where
E: Eq + Hash + Debug + Clone + Display,
P: std::convert::Into<Payload>,
{
init_sdk();
Self {
request: DispatchRequest::new(event, payload.into()),
}
}
// #[allow(dead_code)]
// pub fn bytes_payload<T>(mut self, payload: T) -> Self
// where
// T: serde::Serialize,
// {
// let bytes: Vec<u8> = bincode::serialize(&payload).unwrap();
// self.request = self.request.payload(Payload::Bytes(bytes));
// self
// }
//
// #[allow(dead_code)]
// pub fn protobuf_payload<T>(mut self, payload: T) -> Self
// where
// T: ::protobuf::Message,
// {
// let bytes: Vec<u8> = payload.write_to_bytes().unwrap();
// self.request = self.request.payload(Payload::Bytes(bytes));
// self
// }
#[allow(dead_code)]
pub async fn async_send(self) -> EventResponse {
let resp = async_send(self.request).await;
dbg!(&resp);
resp
}
#[allow(dead_code)]
pub fn sync_send(self) -> EventResponse {
let resp = sync_send(self.request);
dbg!(&resp);
resp
}
}

View File

@ -1,2 +0,0 @@
mod helper;
mod user;

View File

@ -1,23 +0,0 @@
use crate::helper::*;
use flowy_sys::prelude::*;
use flowy_user::prelude::*;
use std::convert::{TryFrom, TryInto};
#[test]
fn sign_in_without_password() {
let params = UserSignInParams {
email: "annie@appflowy.io".to_string(),
password: "".to_string(),
};
let bytes: Vec<u8> = params.try_into().unwrap();
let resp = EventTester::new(SignIn, Payload::Bytes(bytes)).sync_send();
match resp.payload {
Payload::None => {},
Payload::Bytes(bytes) => {
let result = UserSignInResult::try_from(&bytes).unwrap();
dbg!(&result);
},
}
assert_eq!(resp.status_code, StatusCode::Ok);
}

View File

@ -0,0 +1,121 @@
use crate::{
error::{InternalError, SystemError},
request::{unexpected_none_payload, EventRequest, FromRequest, Payload},
response::{EventResponse, Responder, ResponseBuilder, ToBytes},
util::ready::{ready, Ready},
};
use std::ops;
pub struct Data<T>(pub T);
impl<T> Data<T> {
pub fn into_inner(self) -> T { self.0 }
}
impl<T> ops::Deref for Data<T> {
type Target = T;
fn deref(&self) -> &T { &self.0 }
}
impl<T> ops::DerefMut for Data<T> {
fn deref_mut(&mut self) -> &mut T { &mut self.0 }
}
pub trait FromBytes: Sized {
fn parse_from_bytes(bytes: &Vec<u8>) -> Result<Self, String>;
}
#[cfg(feature = "use_protobuf")]
impl<T> FromBytes for T
where
// https://stackoverflow.com/questions/62871045/tryfromu8-trait-bound-in-trait
T: for<'a> std::convert::TryFrom<&'a Vec<u8>, Error = String>,
{
fn parse_from_bytes(bytes: &Vec<u8>) -> Result<Self, String> { T::try_from(bytes) }
}
#[cfg(feature = "use_serde")]
impl<T> FromBytes for T
where
T: serde::de::DeserializeOwned + 'static,
{
fn parse_from_bytes(bytes: &Vec<u8>) -> Result<Self, String> {
let s = String::from_utf8_lossy(bytes);
match serde_json::from_str::<T>(s.as_ref()) {
Ok(data) => Ok(data),
Err(e) => Err(format!("{:?}", e)),
}
}
}
impl<T> FromRequest for Data<T>
where
T: FromBytes + 'static,
{
type Error = SystemError;
type Future = Ready<Result<Self, SystemError>>;
#[inline]
fn from_request(req: &EventRequest, payload: &mut Payload) -> Self::Future {
match payload {
Payload::None => ready(Err(unexpected_none_payload(req))),
Payload::Bytes(bytes) => match T::parse_from_bytes(bytes) {
Ok(data) => ready(Ok(Data(data))),
Err(e) => ready(Err(InternalError::new(format!("{:?}", e)).into())),
},
}
}
}
impl<T> Responder for Data<T>
where
T: ToBytes,
{
fn respond_to(self, _request: &EventRequest) -> EventResponse {
match self.into_inner().into_bytes() {
Ok(bytes) => ResponseBuilder::Ok().data(bytes.to_vec()).build(),
Err(e) => {
let system_err: SystemError = InternalError::new(format!("{:?}", e)).into();
system_err.into()
},
}
}
}
impl<T> std::convert::From<T> for Data<T>
where
T: ToBytes,
{
fn from(val: T) -> Self { Data(val) }
}
impl<T> std::convert::TryFrom<&Payload> for Data<T>
where
T: FromBytes,
{
type Error = String;
fn try_from(payload: &Payload) -> Result<Data<T>, Self::Error> {
match payload {
Payload::None => Err(format!("Expected payload")),
Payload::Bytes(bytes) => match T::parse_from_bytes(bytes) {
Ok(data) => Ok(Data(data)),
Err(e) => Err(e),
},
}
}
}
impl<T> std::convert::TryInto<Payload> for Data<T>
where
T: ToBytes,
{
type Error = String;
fn try_into(self) -> Result<Payload, Self::Error> {
let inner = self.into_inner();
let bytes = inner.into_bytes()?;
Ok(Payload::Bytes(bytes))
}
}

View File

@ -12,6 +12,7 @@ use futures_util::task::Context;
use lazy_static::lazy_static;
use pin_project::pin_project;
use std::{
convert::TryInto,
fmt::{Debug, Display},
future::Future,
hash::Hash,
@ -115,18 +116,33 @@ pub struct DispatchRequest {
}
impl DispatchRequest {
pub fn new<E>(event: E, payload: Payload) -> Self
pub fn new<E>(event: E) -> Self
where
E: Eq + Hash + Debug + Clone + Display,
{
Self {
payload,
payload: Payload::None,
event: event.into(),
id: uuid::Uuid::new_v4().to_string(),
callback: None,
}
}
pub fn payload<P>(mut self, payload: P) -> Self
where
P: TryInto<Payload, Error = String>,
{
let payload = match payload.try_into() {
Ok(payload) => payload,
Err(e) => {
log::error!("{}", e);
Payload::None
},
};
self.payload = payload;
self
}
pub fn callback(mut self, callback: BoxFutureCallback) -> Self {
self.callback = Some(callback);
self

View File

@ -1,5 +1,3 @@
mod error;
pub type ResponseResult<T, E> = std::result::Result<crate::request::Data<T>, E>;
pub use error::*;

View File

@ -8,9 +8,10 @@ mod rt;
mod service;
mod util;
mod data;
mod dispatch;
mod system;
pub mod prelude {
pub use crate::{dispatch::*, error::*, module::*, request::*, response::*, rt::*};
pub use crate::{data::*, dispatch::*, error::*, module::*, request::*, response::*, rt::*};
}

View File

@ -38,6 +38,16 @@ impl std::convert::Into<Payload> for Vec<u8> {
fn into(self) -> Payload { Payload::Bytes(self) }
}
// = note: conflicting implementation in crate `core`:
// - impl<T, U> TryInto<U> for T where U: TryFrom<T>;
//
// impl std::convert::TryInto<Payload> for Vec<u8> {
// type Error = String;
// fn try_into(self) -> Result<Payload, Self::Error> {
// Ok(Payload::Bytes(self))
// }
// }
impl std::convert::Into<Payload> for &str {
fn into(self) -> Payload { self.to_string().into() }
}

View File

@ -10,7 +10,6 @@ use crate::{
use futures_core::ready;
use std::{
fmt::Debug,
ops,
pin::Pin,
task::{Context, Poll},
};
@ -61,7 +60,7 @@ impl FromRequest for String {
}
}
fn unexpected_none_payload(request: &EventRequest) -> SystemError {
pub fn unexpected_none_payload(request: &EventRequest) -> SystemError {
log::warn!("{:?} expected payload", &request.event);
InternalError::new("Expected payload").into()
}
@ -99,65 +98,3 @@ where
Poll::Ready(Ok(res))
}
}
pub struct Data<T>(pub T);
impl<T> Data<T> {
pub fn into_inner(self) -> T { self.0 }
}
impl<T> ops::Deref for Data<T> {
type Target = T;
fn deref(&self) -> &T { &self.0 }
}
impl<T> ops::DerefMut for Data<T> {
fn deref_mut(&mut self) -> &mut T { &mut self.0 }
}
pub trait FromBytes: Sized {
fn parse_from_bytes(bytes: &Vec<u8>) -> Result<Self, String>;
}
#[cfg(feature = "use_protobuf")]
impl<T> FromBytes for T
where
// https://stackoverflow.com/questions/62871045/tryfromu8-trait-bound-in-trait
T: for<'a> std::convert::TryFrom<&'a Vec<u8>, Error = String>,
{
fn parse_from_bytes(bytes: &Vec<u8>) -> Result<Self, String> { T::try_from(bytes) }
}
#[cfg(feature = "use_serde")]
impl<T> FromBytes for T
where
T: serde::de::DeserializeOwned + 'static,
{
fn parse_from_bytes(bytes: &Vec<u8>) -> Result<Self, String> {
let s = String::from_utf8_lossy(bytes);
match serde_json::from_str::<T>(s.as_ref()) {
Ok(data) => Ok(data),
Err(e) => Err(format!("{:?}", e)),
}
}
}
impl<T> FromRequest for Data<T>
where
T: FromBytes + 'static,
{
type Error = SystemError;
type Future = Ready<Result<Self, SystemError>>;
#[inline]
fn from_request(req: &EventRequest, payload: &mut Payload) -> Self::Future {
match payload {
Payload::None => ready(Err(unexpected_none_payload(req))),
Payload::Bytes(bytes) => match T::parse_from_bytes(bytes) {
Ok(data) => ready(Ok(Data(data))),
Err(e) => ready(Err(InternalError::new(format!("{:?}", e)).into())),
},
}
}
}

View File

@ -1,7 +1,7 @@
#[allow(unused_imports)]
use crate::error::{InternalError, SystemError};
use crate::{
request::{Data, EventRequest},
request::EventRequest,
response::{EventResponse, ResponseBuilder},
};
use bytes::Bytes;
@ -62,25 +62,3 @@ where
}
}
}
impl<T> Responder for Data<T>
where
T: ToBytes,
{
fn respond_to(self, _request: &EventRequest) -> EventResponse {
match self.into_inner().into_bytes() {
Ok(bytes) => ResponseBuilder::Ok().data(bytes.to_vec()).build(),
Err(e) => {
let system_err: SystemError = InternalError::new(format!("{:?}", e)).into();
system_err.into()
},
}
}
}
impl<T> std::convert::From<T> for Data<T>
where
T: ToBytes,
{
fn from(val: T) -> Self { Data(val) }
}

View File

@ -1,6 +1,7 @@
use crate::{
data::Data,
error::SystemError,
request::{Data, EventRequest, Payload},
request::{EventRequest, Payload},
response::Responder,
};
use std::{fmt, fmt::Formatter};
@ -51,6 +52,8 @@ impl Responder for EventResponse {
fn respond_to(self, _: &EventRequest) -> EventResponse { self }
}
pub type ResponseResult<T, E> = std::result::Result<Data<T>, E>;
pub fn response_ok<T, E>(data: T) -> Result<Data<T>, E>
where
E: Into<SystemError>,

View File

@ -9,7 +9,7 @@ async fn test_init() {
let event = "1";
init_dispatch(|| vec![Module::new().event(event, hello)]);
let request = DispatchRequest::new(event, Payload::None);
let request = DispatchRequest::new(event);
let resp = async_send(request).await;
dbg!(&resp);
}

View File

@ -0,0 +1,17 @@
[package]
name = "flowy-test"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
flowy-sdk = { path = "../flowy-sdk"}
flowy-sys = { path = "../flowy-sys"}
serde = { version = "1.0", features = ["derive"] }
bincode = { version = "1.3"}
protobuf = {version = "2.24.1"}
claim = "0.5.0"
tokio = { version = "1", features = ["full"]}
futures-util = "0.3.15"

View File

@ -0,0 +1,105 @@
pub use flowy_sdk::*;
use flowy_sys::prelude::*;
use std::{
convert::TryFrom,
fmt::{Debug, Display},
fs,
hash::Hash,
path::PathBuf,
sync::Once,
};
pub mod prelude {
pub use crate::EventTester;
pub use flowy_sys::prelude::*;
pub use std::convert::TryFrom;
}
static INIT: Once = Once::new();
pub fn init_sdk() {
let root_dir = root_dir();
INIT.call_once(|| {
FlowySDK::init_log(&root_dir);
});
FlowySDK::init(&root_dir);
}
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("./".to_owned());
let mut path_buf = fs::canonicalize(&PathBuf::from(&manifest_dir)).unwrap();
path_buf.pop(); // rust-lib
path_buf.push("flowy-test");
path_buf.push("temp");
path_buf.push("flowy");
let root_dir = path_buf.to_str().unwrap().to_string();
if !std::path::Path::new(&root_dir).exists() {
std::fs::create_dir_all(&root_dir).unwrap();
}
root_dir
}
pub struct EventTester {
request: DispatchRequest,
assert_status_code: Option<StatusCode>,
}
impl EventTester {
pub fn new<E>(event: E) -> Self
where
E: Eq + Hash + Debug + Clone + Display,
{
init_sdk();
let request = DispatchRequest::new(event);
Self {
request,
assert_status_code: None,
}
}
pub fn payload<P>(mut self, payload: P) -> Self
where
P: ToBytes,
{
self.request = self.request.payload(Data(payload));
self
}
pub fn assert_status_code(mut self, status_code: StatusCode) -> Self {
self.assert_status_code = Some(status_code);
self
}
#[allow(dead_code)]
pub async fn async_send<R>(self) -> R
where
R: FromBytes,
{
let resp = async_send(self.request).await;
dbg!(&resp);
data_from_response(&resp)
}
pub fn sync_send<R>(self) -> R
where
R: FromBytes,
{
let resp = sync_send(self.request);
if let Some(status_code) = self.assert_status_code {
assert_eq!(resp.status_code, status_code)
}
dbg!(&resp);
data_from_response(&resp)
}
}
fn data_from_response<R>(response: &EventResponse) -> R
where
R: FromBytes,
{
let result = <Data<R>>::try_from(&response.payload).unwrap().into_inner();
result
}

View File

@ -18,9 +18,12 @@ rand = { version = "0.8", features=["std_rng"] }
unicode-segmentation = "1.7.1"
log = "0.4.14"
protobuf = {version = "2.18.0"}
lazy_static = "1.4.0"
fancy-regex = "0.5.0"
[dev-dependencies]
quickcheck = "0.9.2"
quickcheck_macros = "0.9.1"
fake = "~2.3.0"
claim = "0.4.0"
claim = "0.4.0"
flowy-test = { path = "../flowy-test" }

View File

@ -1,6 +1,5 @@
use crate::domain::{UserEmail, UserName, UserPassword};
use flowy_derive::ProtoBuf;
use std::convert::TryInto;
#[derive(ProtoBuf, Default)]
pub struct User {

View File

@ -5,10 +5,14 @@ pub struct UserEmail(pub String);
impl UserEmail {
pub fn parse(s: String) -> Result<UserEmail, String> {
if s.trim().is_empty() {
return Err(format!("Email can not be empty or whitespace"));
}
if validate_email(&s) {
Ok(Self(s))
} else {
Err(format!("{} is not a valid subscriber email.", s))
Err(format!("{} is not a valid email.", s))
}
}
}

View File

@ -19,7 +19,7 @@ impl UserName {
let contains_forbidden_characters = s.chars().any(|g| forbidden_characters.contains(&g));
if is_empty_or_whitespace || is_too_long || contains_forbidden_characters {
Err(format!("{} is not a valid subscriber name.", s))
Err(format!("{} is not a valid name.", s))
} else {
Ok(Self(s))
}

View File

@ -1,6 +1,43 @@
use fancy_regex::Regex;
use lazy_static::lazy_static;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug)]
pub struct UserPassword(pub String);
impl UserPassword {
pub fn parse(s: String) -> Result<UserPassword, String> { Ok(Self(s)) }
pub fn parse(s: String) -> Result<UserPassword, String> {
let is_empty_or_whitespace = s.trim().is_empty();
if is_empty_or_whitespace {
return Err(format!("Password can not be empty or whitespace."));
}
let is_too_long = s.graphemes(true).count() > 100;
let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}'];
let contains_forbidden_characters = s.chars().any(|g| forbidden_characters.contains(&g));
let is_invalid_password = !validate_password(&s);
if is_too_long || contains_forbidden_characters || is_invalid_password {
Err(format!("{} is not a valid password.", s))
} else {
Ok(Self(s))
}
}
}
lazy_static! {
// Test it in https://regex101.com/
// https://stackoverflow.com/questions/2370015/regular-expression-for-password-validation/2370045
// Hell1!
// [invalid, less than 6]
// Hel1!
//
// Hello1!
// [invalid, must include number]
// Hello!
//
// Hello12!
// [invalid must include upper case]
// hello12!
static ref PASSWORD: Regex = Regex::new("((?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\\W]).{6,20})").unwrap();
}
pub fn validate_password(password: &str) -> bool { PASSWORD.is_match(password).is_ok() }

View File

@ -1,13 +0,0 @@
use derive_more::Display;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash)]
pub enum UserEvent {
#[display(fmt = "AuthCheck")]
AuthCheck = 0,
#[display(fmt = "SignIn")]
SignIn = 1,
#[display(fmt = "SignUp")]
SignUp = 2,
#[display(fmt = "SignOut")]
SignOut = 3,
}

View File

@ -1,14 +1,9 @@
mod domain;
mod error;
pub mod event;
mod handlers;
pub mod module;
mod protobuf;
pub mod prelude {
pub use crate::{
domain::*,
event::{UserEvent::*, *},
handlers::auth::*,
};
pub use crate::{domain::*, handlers::auth::*, module::UserEvent::*};
}

View File

@ -1,9 +1,23 @@
use crate::{event::UserEvent::*, handlers::*};
use crate::handlers::*;
use flowy_sys::prelude::*;
use derive_more::Display;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash)]
pub enum UserEvent {
#[display(fmt = "AuthCheck")]
AuthCheck = 0,
#[display(fmt = "SignIn")]
SignIn = 1,
#[display(fmt = "SignUp")]
SignUp = 2,
#[display(fmt = "SignOut")]
SignOut = 3,
}
pub fn create() -> Module {
Module::new()
.name("Flowy-User")
.event(SignIn, user_sign_in)
.event(SignUp, user_sign_up)
.event(UserEvent::SignIn, user_sign_in)
.event(UserEvent::SignUp, user_sign_up)
}

View File

@ -0,0 +1,32 @@
use flowy_test::prelude::*;
use flowy_user::prelude::*;
#[test]
#[should_panic]
fn sign_in_without_password() {
let params = UserSignInParams {
email: "annie@appflowy.io".to_string(),
password: "".to_string(),
};
let result = EventTester::new(SignIn)
.payload(params)
.assert_status_code(StatusCode::Err)
.sync_send::<UserSignInResult>();
dbg!(&result);
}
#[test]
#[should_panic]
fn sign_in_without_email() {
let params = UserSignInParams {
email: "".to_string(),
password: "HelloWorld!123".to_string(),
};
let result = EventTester::new(SignIn)
.payload(params)
.assert_status_code(StatusCode::Err)
.sync_send::<UserSignInResult>();
dbg!(&result);
}