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()),
}
}
}
@@ -73,7 +73,6 @@ pub async fn post_create_testimonial(
Extension(authenticated_user): Extension<UsersDetailQueryDto>,
Json(payload): Json<TestimonialsCreateRequestDto>,
) -> impl IntoResponse {
println!("Authenticated User Now: {:?}", authenticated_user);
TestimonialsService::create_testimonial(&state, payload, &authenticated_user).await
}
@@ -34,7 +34,7 @@ pub struct TestimonialsUpdateRequestDto {
pub struct TestimonialsListItemDto {
pub id: String,
pub user_id: String,
pub user_fullname: String, // Assuming we'll fetch user's full name
pub user_fullname: String,
pub role: String,
pub content: String,
pub created_at: String,
@@ -45,7 +45,7 @@ pub struct TestimonialsListItemDto {
pub struct TestimonialsDetailItemDto {
pub id: String,
pub user_id: String,
pub user_fullname: String, // Assuming we'll fetch user's full name
pub user_fullname: String,
pub role: String,
pub content: String,
pub created_at: String,
@@ -55,7 +55,7 @@ pub struct TestimonialsDetailItemDto {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TestimonialsQueryDto {
pub id: Thing,
pub user: UsersSchema, // Change from Thing to UsersSchema
pub user: UsersSchema,
pub role: String,
pub content: String,
pub is_deleted: bool,
@@ -68,7 +68,7 @@ impl TestimonialsQueryDto {
TestimonialsListItemDto {
id: self.id.id.to_raw(),
user_id: self.user.id.id.to_raw(),
user_fullname: self.user.fullname, // Extract fullname from UsersSchema
user_fullname: self.user.fullname,
role: self.role,
content: self.content,
created_at: self.created_at,
@@ -4,6 +4,9 @@ use super::{
use anyhow::{Result, bail};
use imphnen_libs::{AppState, MetaRequestDto, ResourceEnum, ResponseListSuccessDto};
use imphnen_utils::{DetailQueryBuilder, ListQueryBuilder, get_id, get_iso_date};
use serde_json;
use std::time::Instant;
use tracing::instrument;
pub struct TestimonialsRepository<'a> {
state: &'a AppState,
@@ -14,17 +17,25 @@ impl<'a> TestimonialsRepository<'a> {
Self { state }
}
#[instrument(skip(self, meta), err)]
pub async fn query_testimonial_list(
&self,
meta: MetaRequestDto,
) -> Result<ResponseListSuccessDto<Vec<TestimonialsQueryDto>>> {
let query = ListQueryBuilder::new(&ResourceEnum::Testimonials.to_string())
.with_select_fields(vec!["*", "user.* as user"]) // Select user details
let now = Instant::now();
let query = ListQueryBuilder::new(ResourceEnum::Testimonials.to_string())
.with_select_fields(vec!["*", "user.* as user"])
.with_pagination(meta.page, Some(10))
.with_sorting(meta.sort_by.as_deref(), meta.order.as_deref())
.build();
let res: Vec<TestimonialsQueryDto> =
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_testimonial_list' took: {elapsed:.2?}");
}
let data = ResponseListSuccessDto {
data: res,
meta: None,
@@ -32,17 +43,26 @@ impl<'a> TestimonialsRepository<'a> {
Ok(data)
}
#[instrument(skip(self, id), err)]
pub async fn query_testimonial_by_id(
&self,
id: String,
) -> Result<TestimonialsQueryDto> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let builder = DetailQueryBuilder::new(ResourceEnum::Testimonials.to_string())
.with_id(&id)
.with_select_fields(vec!["*", "user.* as user"]); // Select user details
.with_condition("is_deleted = false")
.with_select_fields(vec!["*", "user.* as user"]);
let sql = builder.build();
let result: Option<TestimonialsQueryDto> =
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_testimonial_by_id' took: {elapsed:.2?}");
}
match result {
Some(testimonial) => {
@@ -55,15 +75,23 @@ impl<'a> TestimonialsRepository<'a> {
}
}
#[instrument(skip(self, data), err)]
pub async fn query_create_testimonial(
&self,
data: TestimonialsSchema,
) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let record: Option<TestimonialsSchema> = db
.create(ResourceEnum::Testimonials.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_testimonial' took: {elapsed:.2?}");
}
match record {
Some(_) => Ok("Success create testimonial".into()),
@@ -71,10 +99,12 @@ impl<'a> TestimonialsRepository<'a> {
}
}
#[instrument(skip(self, data), err)]
pub async fn query_update_testimonial(
&self,
data: TestimonialsSchema,
) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let existing = self.query_testimonial_by_id(data.id.id.to_raw()).await?;
@@ -85,13 +115,19 @@ impl<'a> TestimonialsRepository<'a> {
let merged = TestimonialsSchema {
created_at: existing.created_at,
updated_at: get_iso_date(),
user: existing.user.id, // Preserve user ID
user: existing.user.id,
..data
};
let record_key = get_id(&merged.id)?;
let record: Option<TestimonialsSchema> =
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_testimonial' took: {elapsed:.2?}");
}
match record {
Some(_) => Ok("Success update testimonial".into()),
@@ -99,7 +135,9 @@ impl<'a> TestimonialsRepository<'a> {
}
}
#[instrument(skip(self, id), err)]
pub async fn query_delete_testimonial(&self, id: String) -> Result<String> {
let now = Instant::now();
let db = &self.state.surrealdb_ws;
let testimonial = self.query_testimonial_by_id(id).await?;
if testimonial.is_deleted {
@@ -111,6 +149,12 @@ impl<'a> TestimonialsRepository<'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_testimonial' took: {elapsed:.2?}");
}
match record {
Some(_) => Ok("Success delete testimonial".into()),
@@ -11,7 +11,7 @@ use super::testimonials_dto::{
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TestimonialsSchema {
pub id: Thing,
pub user: Thing, // Link to app_users table
pub user: Thing,
pub role: String,
pub content: String,
pub is_deleted: bool,
@@ -28,7 +28,7 @@ impl Default for TestimonialsSchema {
),
user: make_thing(
&ResourceEnum::Users.to_string(),
&Uuid::new_v4().to_string(), // Placeholder, will be replaced by actual user ID
&Uuid::new_v4().to_string(),
),
role: String::new(),
content: String::new(),