diff --git a/src/database/mock.rs b/src/database/mock.rs index dbef72a4..a8ba80e6 100644 --- a/src/database/mock.rs +++ b/src/database/mock.rs @@ -41,8 +41,14 @@ impl MockDatabase { self } - pub fn append_query_results(mut self, mut vec: Vec>) -> Self { - self.query_results.append(&mut vec); + pub fn append_query_results(mut self, vec: Vec>) -> Self + where + T: IntoMockRow, + { + for row in vec.into_iter() { + let row = row.into_iter().map(|vec| vec.into_mock_row()).collect(); + self.query_results.push(row); + } self } @@ -80,6 +86,19 @@ impl MockDatabaseTrait for MockDatabase { Err(QueryErr) } } + + fn into_transaction_log(&mut self) -> Vec { + std::mem::take(&mut self.transaction_log) + } + + fn assert_transaction_log(&mut self, stmts: Vec) { + for stmt in stmts.iter() { + assert!(!self.transaction_log.is_empty()); + let log = self.transaction_log.first().unwrap(); + assert_eq!(log.to_string(), stmt.to_string()); + self.transaction_log = self.transaction_log.drain(1..).collect(); + } + } } impl MockRow { @@ -102,3 +121,16 @@ impl From> for MockRow { } } } + +pub trait IntoMockRow { + fn into_mock_row(self) -> MockRow; +} + +impl IntoMockRow for T +where + T: Into, +{ + fn into_mock_row(self) -> MockRow { + self.into() + } +} diff --git a/src/driver/mock.rs b/src/driver/mock.rs index 7352bc44..8be8478b 100644 --- a/src/driver/mock.rs +++ b/src/driver/mock.rs @@ -10,14 +10,18 @@ use std::sync::{ pub struct MockDatabaseConnector; pub struct MockDatabaseConnection { - counter: AtomicUsize, - mocker: Mutex>, + pub(crate) counter: AtomicUsize, + pub(crate) mocker: Mutex>, } pub trait MockDatabaseTrait: Send { fn execute(&mut self, counter: usize, stmt: Statement) -> Result; fn query(&mut self, counter: usize, stmt: Statement) -> Result, QueryErr>; + + fn into_transaction_log(&mut self) -> Vec; + + fn assert_transaction_log(&mut self, stmts: Vec); } impl MockDatabaseConnector { diff --git a/src/executor/paginator.rs b/src/executor/paginator.rs index 3196248e..b159ee7b 100644 --- a/src/executor/paginator.rs +++ b/src/executor/paginator.rs @@ -99,3 +99,239 @@ where }) } } + +#[cfg(test)] +#[cfg(feature = "mock")] +mod tests { + use crate::entity::prelude::*; + use crate::tests_cfg::{util::*, *}; + use crate::{Database, MockDatabase, QueryErr}; + use futures::TryStreamExt; + use sea_query::{Alias, Expr, SelectStatement, Value}; + + fn setup() -> (Database, Vec>) { + let page1 = vec![ + fruit::Model { + id: 1, + name: "Blueberry".into(), + cake_id: Some(1), + }, + fruit::Model { + id: 2, + name: "Rasberry".into(), + cake_id: Some(1), + }, + ]; + + let page2 = vec![fruit::Model { + id: 3, + name: "Strawberry".into(), + cake_id: Some(2), + }]; + + let page3 = Vec::::new(); + + let db = MockDatabase::new() + .append_query_results(vec![page1.clone(), page2.clone(), page3.clone()]) + .into_database(); + + (db, vec![page1, page2, page3]) + } + + fn setup_num_rows() -> (Database, i32) { + let num_rows = 3; + let db = MockDatabase::new() + .append_query_results(vec![vec![maplit::btreemap! { + "num_rows" => Into::::into(num_rows), + }]]) + .into_database(); + + (db, num_rows) + } + + #[async_std::test] + async fn fetch_page() -> Result<(), QueryErr> { + let (db, pages) = setup(); + + let paginator = fruit::Entity::find().paginate(&db, 2); + + assert_eq!(paginator.fetch_page(0).await?, pages[0].clone()); + assert_eq!(paginator.fetch_page(1).await?, pages[1].clone()); + assert_eq!(paginator.fetch_page(2).await?, pages[2].clone()); + + let select = SelectStatement::new() + .exprs(vec![ + Expr::tbl(fruit::Entity, fruit::Column::Id), + Expr::tbl(fruit::Entity, fruit::Column::Name), + Expr::tbl(fruit::Entity, fruit::Column::CakeId), + ]) + .from(fruit::Entity) + .to_owned(); + + let query_builder = db.get_query_builder_backend(); + let stmts = vec![ + query_builder.build_select_statement(select.clone().offset(0).limit(2)), + query_builder.build_select_statement(select.clone().offset(2).limit(2)), + query_builder.build_select_statement(select.clone().offset(4).limit(2)), + ]; + let mut mocker = get_mock_db_connection(&db).mocker.lock().unwrap(); + mocker.assert_transaction_log(stmts); + + Ok(()) + } + + #[async_std::test] + async fn fetch() -> Result<(), QueryErr> { + let (db, pages) = setup(); + + let mut paginator = fruit::Entity::find().paginate(&db, 2); + + assert_eq!(paginator.fetch().await?, pages[0].clone()); + paginator.next(); + + assert_eq!(paginator.fetch().await?, pages[1].clone()); + paginator.next(); + + assert_eq!(paginator.fetch().await?, pages[2].clone()); + + let select = SelectStatement::new() + .exprs(vec![ + Expr::tbl(fruit::Entity, fruit::Column::Id), + Expr::tbl(fruit::Entity, fruit::Column::Name), + Expr::tbl(fruit::Entity, fruit::Column::CakeId), + ]) + .from(fruit::Entity) + .to_owned(); + + let query_builder = db.get_query_builder_backend(); + let stmts = vec![ + query_builder.build_select_statement(select.clone().offset(0).limit(2)), + query_builder.build_select_statement(select.clone().offset(2).limit(2)), + query_builder.build_select_statement(select.clone().offset(4).limit(2)), + ]; + let mut mocker = get_mock_db_connection(&db).mocker.lock().unwrap(); + mocker.assert_transaction_log(stmts); + + Ok(()) + } + + #[async_std::test] + async fn num_pages() -> Result<(), QueryErr> { + let (db, num_rows) = setup_num_rows(); + + let num_rows = num_rows as usize; + let page_size = 2_usize; + let num_pages = (num_rows / page_size) + (num_rows % page_size > 0) as usize; + let paginator = fruit::Entity::find().paginate(&db, page_size); + + assert_eq!(paginator.num_pages().await?, num_pages); + + let sub_query = SelectStatement::new() + .exprs(vec![ + Expr::tbl(fruit::Entity, fruit::Column::Id), + Expr::tbl(fruit::Entity, fruit::Column::Name), + Expr::tbl(fruit::Entity, fruit::Column::CakeId), + ]) + .from(fruit::Entity) + .to_owned(); + + let select = SelectStatement::new() + .expr(Expr::cust("COUNT(*) AS num_rows")) + .from_subquery(sub_query, Alias::new("sub_query")) + .to_owned(); + + let query_builder = db.get_query_builder_backend(); + let stmts = vec![query_builder.build_select_statement(&select)]; + let mut mocker = get_mock_db_connection(&db).mocker.lock().unwrap(); + mocker.assert_transaction_log(stmts); + + Ok(()) + } + + #[async_std::test] + async fn next_and_cur_page() -> Result<(), QueryErr> { + let (db, _) = setup(); + + let mut paginator = fruit::Entity::find().paginate(&db, 2); + + assert_eq!(paginator.cur_page(), 0); + paginator.next(); + + assert_eq!(paginator.cur_page(), 1); + paginator.next(); + + assert_eq!(paginator.cur_page(), 2); + + Ok(()) + } + + #[async_std::test] + async fn fetch_and_next() -> Result<(), QueryErr> { + let (db, pages) = setup(); + + let mut paginator = fruit::Entity::find().paginate(&db, 2); + + assert_eq!(paginator.cur_page(), 0); + assert_eq!(paginator.fetch_and_next().await?, Some(pages[0].clone())); + + assert_eq!(paginator.cur_page(), 1); + assert_eq!(paginator.fetch_and_next().await?, Some(pages[1].clone())); + + assert_eq!(paginator.cur_page(), 2); + assert_eq!(paginator.fetch_and_next().await?, None); + + let select = SelectStatement::new() + .exprs(vec![ + Expr::tbl(fruit::Entity, fruit::Column::Id), + Expr::tbl(fruit::Entity, fruit::Column::Name), + Expr::tbl(fruit::Entity, fruit::Column::CakeId), + ]) + .from(fruit::Entity) + .to_owned(); + + let query_builder = db.get_query_builder_backend(); + let stmts = vec![ + query_builder.build_select_statement(select.clone().offset(0).limit(2)), + query_builder.build_select_statement(select.clone().offset(2).limit(2)), + query_builder.build_select_statement(select.clone().offset(4).limit(2)), + ]; + let mut mocker = get_mock_db_connection(&db).mocker.lock().unwrap(); + mocker.assert_transaction_log(stmts); + + Ok(()) + } + + #[async_std::test] + async fn into_stream() -> Result<(), QueryErr> { + let (db, pages) = setup(); + + let mut fruit_stream = fruit::Entity::find().paginate(&db, 2).into_stream(); + + assert_eq!(fruit_stream.try_next().await?, Some(pages[0].clone())); + assert_eq!(fruit_stream.try_next().await?, Some(pages[1].clone())); + assert_eq!(fruit_stream.try_next().await?, None); + + drop(fruit_stream); + + let select = SelectStatement::new() + .exprs(vec![ + Expr::tbl(fruit::Entity, fruit::Column::Id), + Expr::tbl(fruit::Entity, fruit::Column::Name), + Expr::tbl(fruit::Entity, fruit::Column::CakeId), + ]) + .from(fruit::Entity) + .to_owned(); + + let query_builder = db.get_query_builder_backend(); + let stmts = vec![ + query_builder.build_select_statement(select.clone().offset(0).limit(2)), + query_builder.build_select_statement(select.clone().offset(2).limit(2)), + query_builder.build_select_statement(select.clone().offset(4).limit(2)), + ]; + let mut mocker = get_mock_db_connection(&db).mocker.lock().unwrap(); + mocker.assert_transaction_log(stmts[0..1].to_vec()); + mocker.assert_transaction_log(stmts[1..].to_vec()); + + Ok(()) + } +} diff --git a/src/query/json.rs b/src/query/json.rs index 0487dc69..838c5ffa 100644 --- a/src/query/json.rs +++ b/src/query/json.rs @@ -72,8 +72,7 @@ mod tests { let db = MockDatabase::new() .append_query_results(vec![vec![maplit::btreemap! { "id" => Into::::into(128), "name" => Into::::into("apple") - } - .into()]]) + }]]) .into_database(); assert_eq!( diff --git a/src/tests_cfg/mod.rs b/src/tests_cfg/mod.rs index e39a16f3..70660dd8 100644 --- a/src/tests_cfg/mod.rs +++ b/src/tests_cfg/mod.rs @@ -4,3 +4,7 @@ pub mod cake; pub mod cake_filling; pub mod filling; pub mod fruit; + +#[cfg(test)] +#[cfg(feature = "mock")] +pub mod util; diff --git a/src/tests_cfg/util.rs b/src/tests_cfg/util.rs new file mode 100644 index 00000000..1927e9ad --- /dev/null +++ b/src/tests_cfg/util.rs @@ -0,0 +1,52 @@ +use crate::{ + tests_cfg::*, Database, DatabaseConnection, IntoMockRow, MockDatabaseConnection, MockRow, +}; +use sea_query::Value; + +impl From for MockRow { + fn from(model: cake_filling::Model) -> Self { + let map = maplit::btreemap! { + "cake_id" => Into::::into(model.cake_id), + "filling_id" => Into::::into(model.filling_id), + }; + map.into_mock_row() + } +} + +impl From for MockRow { + fn from(model: cake::Model) -> Self { + let map = maplit::btreemap! { + "id" => Into::::into(model.id), + "name" => Into::::into(model.name), + }; + map.into_mock_row() + } +} + +impl From for MockRow { + fn from(model: filling::Model) -> Self { + let map = maplit::btreemap! { + "id" => Into::::into(model.id), + "name" => Into::::into(model.name), + }; + map.into_mock_row() + } +} + +impl From for MockRow { + fn from(model: fruit::Model) -> Self { + let map = maplit::btreemap! { + "id" => Into::::into(model.id), + "name" => Into::::into(model.name), + "cake_id" => Into::::into(model.cake_id), + }; + map.into_mock_row() + } +} + +pub fn get_mock_db_connection(db: &Database) -> &MockDatabaseConnection { + match db.get_connection() { + DatabaseConnection::MockDatabaseConnection(mock_conn) => mock_conn, + _ => unreachable!(), + } +}