Add comprehensive tests for mentor repository and authentication
- Implemented tests for creating, retrieving, updating, and deleting mentors in `mentor_repository_test.rs`. - Added tests for user authentication, including successful login, invalid email formats, and inactive users in `auth_login_tests.rs`. - Created a mock test environment setup in `mock_test.rs` to facilitate database operations during tests. - Updated module structure to include new test files for mentors and authentication. - Ensured cleanup of the database after tests to maintain isolation and prevent side effects.
This commit is contained in:
+122
-97
@@ -1,117 +1,142 @@
|
||||
use crate::{CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto};
|
||||
use anyhow::Result;
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use surrealdb::Surreal;
|
||||
use imphnen_entities::{
|
||||
CountResult, MetaRequestDto, MetaResponseDto, ResponseListSuccessDto,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use surrealdb::engine::any;
|
||||
use surrealdb::Surreal;
|
||||
use tracing;
|
||||
|
||||
pub struct QueryListBuilder<'a> {
|
||||
db: &'a Surreal<any::Any>,
|
||||
table: &'a str,
|
||||
meta: &'a MetaRequestDto,
|
||||
conditions: Vec<String>,
|
||||
search_field: String,
|
||||
select_fields: Option<Vec<&'a str>>,
|
||||
fetch_fields: Option<Vec<&'a str>>,
|
||||
db: &'a Surreal<any::Any>,
|
||||
table: &'a str,
|
||||
meta: &'a MetaRequestDto,
|
||||
conditions: Vec<String>,
|
||||
search_field: String,
|
||||
select_fields: Option<Vec<&'a str>>,
|
||||
fetch_fields: Option<Vec<&'a str>>,
|
||||
cast_thing_fields: bool,
|
||||
}
|
||||
|
||||
impl<'a> QueryListBuilder<'a> {
|
||||
pub fn new(
|
||||
db: &'a Surreal<any::Any>,
|
||||
table: &'a str,
|
||||
meta: &'a MetaRequestDto,
|
||||
) -> Self {
|
||||
Self {
|
||||
db,
|
||||
table,
|
||||
meta,
|
||||
conditions: vec![],
|
||||
search_field: "name".to_string(),
|
||||
select_fields: None,
|
||||
fetch_fields: None,
|
||||
}
|
||||
}
|
||||
pub fn new(
|
||||
db: &'a Surreal<any::Any>,
|
||||
table: &'a str,
|
||||
meta: &'a MetaRequestDto,
|
||||
) -> Self {
|
||||
Self {
|
||||
db,
|
||||
table,
|
||||
meta,
|
||||
conditions: vec![],
|
||||
search_field: "name".to_string(),
|
||||
select_fields: None,
|
||||
fetch_fields: None,
|
||||
cast_thing_fields: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search_field(mut self, field: &'a str) -> Self {
|
||||
self.search_field = field.to_string();
|
||||
self
|
||||
}
|
||||
pub fn search_field(mut self, field: &'a str) -> Self {
|
||||
self.search_field = field.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn select_fields(mut self, fields: Vec<&'a str>) -> Self {
|
||||
self.select_fields = Some(fields);
|
||||
self
|
||||
}
|
||||
pub fn select_fields(mut self, fields: Vec<&'a str>) -> Self {
|
||||
self.select_fields = Some(fields);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fetch_fields(mut self, fields: Vec<&'a str>) -> Self {
|
||||
self.fetch_fields = Some(fields);
|
||||
self
|
||||
}
|
||||
pub fn fetch_fields(mut self, fields: Vec<&'a str>) -> Self {
|
||||
self.fetch_fields = Some(fields);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_condition(mut self, condition: &str) -> Self {
|
||||
self.conditions.push(condition.to_string());
|
||||
self
|
||||
}
|
||||
pub fn with_condition(mut self, condition: &str) -> Self {
|
||||
self.conditions.push(condition.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn build<T>(self) -> Result<ResponseListSuccessDto<Vec<T>>>
|
||||
where
|
||||
T: DeserializeOwned + Serialize,
|
||||
{
|
||||
let page = self.meta.page.unwrap_or(1).max(1);
|
||||
let per_page = self.meta.per_page.unwrap_or(10).max(1);
|
||||
let start = (page - 1) * per_page;
|
||||
pub fn with_cast_thing_fields(mut self) -> Self {
|
||||
self.cast_thing_fields = true;
|
||||
self
|
||||
}
|
||||
|
||||
let sql = crate::ListQueryBuilder::from_meta(
|
||||
self.table,
|
||||
self.meta,
|
||||
&self.search_field,
|
||||
self.select_fields,
|
||||
self.fetch_fields,
|
||||
)
|
||||
.build();
|
||||
pub async fn build<T>(self) -> Result<ResponseListSuccessDto<Vec<T>>>
|
||||
where
|
||||
T: DeserializeOwned + Serialize,
|
||||
{
|
||||
let page = self.meta.page.unwrap_or(1).max(1);
|
||||
let per_page = self.meta.per_page.unwrap_or(10).max(1);
|
||||
let start = (page - 1) * per_page;
|
||||
|
||||
let mut query_exec = self.db.query(sql);
|
||||
if let Some(search) = &self.meta.search {
|
||||
if !search.is_empty() {
|
||||
query_exec = query_exec.bind(("search", search.to_lowercase()));
|
||||
}
|
||||
}
|
||||
if let Some(filter_val) = &self.meta.filter {
|
||||
query_exec = crate::bind_filter_value(query_exec, filter_val.clone());
|
||||
}
|
||||
query_exec = query_exec
|
||||
.bind(("per_page", per_page))
|
||||
.bind(("start", start));
|
||||
// --- Data Query ---
|
||||
let data_query_builder = crate::ListQueryBuilder::from_meta(
|
||||
self.table,
|
||||
self.meta,
|
||||
&self.search_field,
|
||||
self.select_fields,
|
||||
self.fetch_fields,
|
||||
);
|
||||
let data_sql = data_query_builder.build();
|
||||
|
||||
let raw: Vec<T> = query_exec.await?.take(0)?;
|
||||
// --- Count Query ---
|
||||
let count_query_builder = crate::ListQueryBuilder::from_meta(
|
||||
self.table,
|
||||
self.meta,
|
||||
&self.search_field,
|
||||
None, // No select fields for count
|
||||
None, // No fetch fields for count
|
||||
);
|
||||
let count_sql = count_query_builder.build_count();
|
||||
|
||||
let mut count_query = self.db.query(format!(
|
||||
"SELECT count() FROM {} {}",
|
||||
self.table,
|
||||
if self.conditions.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
format!("WHERE {}", self.conditions.join(" AND "))
|
||||
}
|
||||
));
|
||||
// Combine both queries into a single query string within a transaction for a single database call
|
||||
let combined_sql = format!(
|
||||
"BEGIN; {}; {}; COMMIT;",
|
||||
data_sql,
|
||||
count_sql
|
||||
);
|
||||
|
||||
if let Some(search) = &self.meta.search {
|
||||
if !search.is_empty() {
|
||||
count_query = count_query.bind(("search", search.clone()));
|
||||
}
|
||||
}
|
||||
if let Some(filter_val) = &self.meta.filter {
|
||||
count_query = crate::bind_filter_value(count_query, filter_val.clone());
|
||||
}
|
||||
let mut query_exec = self.db.query(combined_sql.clone());
|
||||
|
||||
let count_result: Vec<CountResult> = count_query.await?.take(0)?;
|
||||
let total = count_result.first().map(|c| c.count);
|
||||
// Bind parameters for both data and count queries.
|
||||
// It's assumed that the parameters are named consistently and applied to both.
|
||||
// The ListQueryBuilder already uses $search, $per_page, $start, $filter.
|
||||
if let Some(search) = &self.meta.search {
|
||||
if !search.is_empty() {
|
||||
query_exec = query_exec.bind(("search", search.to_lowercase()));
|
||||
}
|
||||
}
|
||||
if let Some(filter_val) = &self.meta.filter {
|
||||
query_exec = crate::bind_filter_value(query_exec, filter_val.clone());
|
||||
}
|
||||
query_exec = query_exec
|
||||
.bind(("per_page", per_page))
|
||||
.bind(("start", start));
|
||||
|
||||
Ok(ResponseListSuccessDto {
|
||||
data: raw,
|
||||
meta: Some(MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total,
|
||||
}),
|
||||
})
|
||||
}
|
||||
let query_debug_str = format!("{:?}", &query_exec);
|
||||
|
||||
let mut response = query_exec.await.map_err(|e| {
|
||||
tracing::error!(
|
||||
query = %combined_sql, // `combined_sql` is cloned, so it can be borrowed here
|
||||
full_query_object = %query_debug_str,
|
||||
"Failed to execute combined query: {:?}", e
|
||||
);
|
||||
e
|
||||
})?;
|
||||
|
||||
// Extract results: first for the data, then for the count
|
||||
let raw: Vec<T> = response.take(0)?; // First result is the data
|
||||
let count_result: Vec<CountResult> = response.take(1)?; // Second result is the count
|
||||
|
||||
let total = count_result.first().map(|c| c.count);
|
||||
|
||||
Ok(ResponseListSuccessDto {
|
||||
data: raw,
|
||||
meta: Some(MetaResponseDto {
|
||||
page: Some(page),
|
||||
per_page: Some(per_page),
|
||||
total,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user