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:
MythEclipse
2025-07-21 21:29:04 +07:00
parent e66f1f1634
commit 1a2e0c58b6
103 changed files with 7851 additions and 1509 deletions
+11 -14
View File
@@ -1,16 +1,14 @@
use chrono::{DateTime, Utc};
use lazy_static::lazy_static;
use regex::Regex;
lazy_static! {
pub static ref VALID_URL_REGEX: regex::Regex =
regex::Regex::new(r"^https?://").unwrap();
}
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use surrealdb::sql::Thing;
use utoipa::ToSchema;
use validator::Validate;
// Lazy static regex for URL validation
lazy_static! {
static ref VALID_URL_REGEX: Regex = Regex::new(r"^https?://").unwrap();
}
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema, Validate)]
pub struct EventsCreateRequestDto {
#[validate(length(min = 1, message = "Name is required"))]
@@ -19,13 +17,10 @@ pub struct EventsCreateRequestDto {
#[validate(length(min = 1, message = "Description is required"))]
pub description: String,
#[validate(regex(
path = "VALID_URL_REGEX",
message = "Detail link must be a valid URL"
))]
#[validate(url(message = "Detail link must be a valid URL"))]
pub detail_link: String,
#[validate(range(min = 0, message = "Price cannot be negative"))]
#[validate(range(min = 0.0, message = "Price cannot be negative"))]
pub price: f64,
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
@@ -42,16 +37,18 @@ pub struct EventsCreateRequestDto {
pub struct EventsUpdateRequestDto {
#[validate(length(min = 1, message = "Name is required"))]
pub name: String,
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
pub end_date: DateTime<Utc>,
#[schema(example = "2025-09-20T13:00:00Z", value_type = String)]
pub start_date: DateTime<Utc>,
#[validate(range(min = 0.0, message = "Price cannot be negative"))]
pub price: f64,
pub is_online: bool,
pub description: String,
#[validate(url(message = "Detail link must be a valid URL"))]
pub detail_link: String,
pub location: Option<String>,
}
@@ -2,6 +2,8 @@ use super::{events_dto::EventsQueryDto, events_schema::EventsSchema};
use anyhow::{Result, bail};
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
use std::time::Instant;
use tracing::instrument;
pub struct EventsRepository<'a> {
state: &'a AppState,
@@ -12,17 +14,25 @@ impl<'a> EventsRepository<'a> {
Self { state }
}
#[instrument(skip(self, meta), err)]
pub async fn query_event_list(
&self,
meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<EventsQueryDto>>> {
let query = ListQueryBuilder::new(&ResourceEnum::Events.to_string())
let now = Instant::now();
let query = ListQueryBuilder::new(ResourceEnum::Events.to_string())
.with_select_fields(vec!["*"])
.with_pagination(meta.page, Some(10))
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
.build();
let res: Vec<EventsQueryDto> =
self.state.surrealdb_ws.query(query).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_event_list' took: {elapsed:.2?}");
}
let data = ResponseListSuccessDto {
data: res,
meta: None,
@@ -30,8 +40,9 @@ impl<'a> EventsRepository<'a> {
Ok(data)
}
// Get event by ID
#[instrument(skip(self, id), err)]
pub async fn query_event_by_id(&self, id: String) -> Result<EventsQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let builder = DetailQueryBuilder::new(ResourceEnum::Events.to_string())
.with_id(&id)
@@ -39,6 +50,12 @@ impl<'a> EventsRepository<'a> {
let sql = builder.build();
let result: Option<EventsQueryDto> =
builder.apply_bindings(db.query(sql)).await?.take(0)?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_event_by_id' took: {elapsed:.2?}");
}
match result {
Some(event) => {
@@ -51,13 +68,20 @@ impl<'a> EventsRepository<'a> {
}
}
// Create new event
#[instrument(skip(self, data), err)]
pub async fn query_create_event(&self, data: EventsSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let record: Option<EventsSchema> = db
.create(ResourceEnum::Events.to_string())
.content(data)
.await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_create_event' took: {elapsed:.2?}");
}
match record {
Some(_) => Ok("Success create event".into()),
@@ -65,17 +89,16 @@ impl<'a> EventsRepository<'a> {
}
}
// Update existing event
#[instrument(skip(self, data), err)]
pub async fn query_update_event(&self, data: EventsSchema) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
// Cek apakah event ada
let existing = self.query_event_by_id(data.id.id.to_raw()).await?;
if existing.is_deleted {
bail!("Event already deleted");
}
// Merge field tertentu jika diperlukan
let merged = EventsSchema {
created_at: existing.created_at,
updated_at: get_iso_date(),
@@ -84,6 +107,12 @@ impl<'a> EventsRepository<'a> {
let record_key = get_id(&merged.id)?;
let record: Option<EventsSchema> = db.update(record_key).merge(merged).await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_update_event' took: {elapsed:.2?}");
}
match record {
Some(_) => Ok("Success update event".into()),
@@ -91,8 +120,9 @@ impl<'a> EventsRepository<'a> {
}
}
// Soft delete event (mark is_deleted = true)
#[instrument(skip(self, id), err)]
pub async fn query_delete_event(&self, id: String) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let event = self.query_event_by_id(id).await?;
if event.is_deleted {
@@ -104,6 +134,12 @@ impl<'a> EventsRepository<'a> {
.update(record_key)
.merge(serde_json::json!({ "is_deleted": true }))
.await?;
let elapsed = now.elapsed();
if std::env::var("RUST_ENV").unwrap_or_else(|_| "development".to_string())
== "development"
{
println!("Query 'query_delete_event' took: {elapsed:.2?}");
}
match record {
Some(_) => Ok("Success delete event".into()),
@@ -1,10 +1,12 @@
use imphnen_libs::ResourceEnum;
use imphnen_utils::{get_iso_date, make_thing};
use serde::{Deserialize, Serialize};
use surrealdb::Uuid;
use surrealdb::sql::Thing;
use serde::{Deserialize, Serialize};
use imphnen_utils::{get_iso_date, make_thing};
use super::events_dto::{EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto};
use super::events_dto::{
EventsCreateRequestDto, EventsQueryDto, EventsUpdateRequestDto,
};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EventsSchema {
@@ -1,86 +1,101 @@
use super::{
events_dto::{EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto, EventsUpdateRequestDto},
events_repository::EventsRepository,
events_schema::EventsSchema,
events_dto::{
EventsCreateRequestDto, EventsDetailItemDto, EventsListItemDto, EventsQueryDto,
EventsUpdateRequestDto,
},
events_repository::EventsRepository,
events_schema::EventsSchema,
};
use imphnen_libs::{AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto};
use imphnen_utils::{common_response, success_list_response, success_response, validate_request};
use axum::{http::StatusCode, response::Response};
use imphnen_libs::{
AppState, MetaRequestDto, ResponseListSuccessDto, ResponseSuccessDto,
};
use imphnen_utils::{
common_response, success_list_response, success_response, validate_request,
};
pub struct EventsService;
impl EventsService {
pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response {
let repo = EventsRepository::new(state);
match repo.query_event_list(meta).await {
Ok(data) => {
let items: Vec<EventsListItemDto> = data.data
.into_iter()
.filter(|e| !e.is_deleted)
.map(EventsQueryDto::from)
.collect();
let response = ResponseListSuccessDto {
data: items,
meta: data.meta,
};
success_list_response(response)
}
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
}
}
pub async fn get_event_list(state: &AppState, meta: MetaRequestDto) -> Response {
let repo = EventsRepository::new(state);
match repo.query_event_list(meta).await {
Ok(data) => {
let items: Vec<EventsListItemDto> = data
.data
.into_iter()
.filter(|e| !e.is_deleted)
.map(EventsQueryDto::from)
.collect();
let response = ResponseListSuccessDto {
data: items,
meta: data.meta,
};
success_list_response(response)
}
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
}
}
pub async fn get_event_by_id(state: &AppState, id: String) -> Response {
let repo = EventsRepository::new(state);
match repo.query_event_by_id(id).await {
Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto {
data: EventsDetailItemDto {
id: event.id.id.to_raw(),
name: event.name,
description: event.description,
detail_link: event.detail_link,
price: event.price,
is_online: event.is_online,
start_date: event.start_date,
end_date: event.end_date,
created_at: event.created_at,
updated_at: event.updated_at,
location: event.location,
},
}),
Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"),
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
}
}
pub async fn get_event_by_id(state: &AppState, id: String) -> Response {
let repo = EventsRepository::new(state);
match repo.query_event_by_id(id).await {
Ok(event) if !event.is_deleted => success_response(ResponseSuccessDto {
data: EventsDetailItemDto {
id: event.id.id.to_raw(),
name: event.name,
description: event.description,
detail_link: event.detail_link,
price: event.price,
is_online: event.is_online,
start_date: event.start_date,
end_date: event.end_date,
created_at: event.created_at,
updated_at: event.updated_at,
location: event.location,
},
}),
Ok(_) => common_response(StatusCode::NOT_FOUND, "Event not found"),
Err(e) => common_response(StatusCode::NOT_FOUND, &e.to_string()),
}
}
pub async fn create_event(state: &AppState, payload: EventsCreateRequestDto) -> Response {
if let Err((status, message)) = validate_request(&payload) {
return common_response(status, &message);
}
let repo = EventsRepository::new(state);
let schema = EventsSchema::create(payload);
match repo.query_create_event(schema).await {
Ok(msg) => common_response(StatusCode::CREATED, &msg),
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
pub async fn create_event(
state: &AppState,
payload: EventsCreateRequestDto,
) -> Response {
if let Err((status, message)) = validate_request(&payload) {
return common_response(status, &message);
}
let repo = EventsRepository::new(state);
let schema = EventsSchema::create(payload);
match repo.query_create_event(schema).await {
Ok(msg) => common_response(StatusCode::CREATED, &msg),
Err(e) => common_response(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()),
}
}
pub async fn update_event(state: &AppState, id: String, payload: EventsUpdateRequestDto) -> Response {
if let Err((status, message)) = validate_request(&payload) {
return common_response(status, &message);
}
let repo = EventsRepository::new(state);
let schema = EventsSchema::update(payload, id);
match repo.query_update_event(schema).await {
Ok(msg) => common_response(StatusCode::OK, &msg),
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
}
}
pub async fn update_event(
state: &AppState,
id: String,
payload: EventsUpdateRequestDto,
) -> Response {
if let Err((status, message)) = validate_request(&payload) {
return common_response(status, &message);
}
let repo = EventsRepository::new(state);
let schema = EventsSchema::update(payload, id);
match repo.query_update_event(schema).await {
Ok(msg) => common_response(StatusCode::OK, &msg),
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
}
}
pub async fn delete_event(state: &AppState, id: String) -> Response {
let repo = EventsRepository::new(state);
match repo.query_delete_event(id).await {
Ok(msg) => common_response(StatusCode::OK, &msg),
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
}
}
}
pub async fn delete_event(state: &AppState, id: String) -> Response {
let repo = EventsRepository::new(state);
match repo.query_delete_event(id).await {
Ok(msg) => common_response(StatusCode::OK, &msg),
Err(e) => common_response(StatusCode::BAD_REQUEST, &e.to_string()),
}
}
}