Add minimal test for basic hackathon operations and enhance test utilities
- Introduced a new test module for hackathon-related functionality. - Implemented a basic test for creating a hackathon using a mock repository. - Enhanced the test utilities in `lib.rs` for better request handling and response extraction. - Added a `ServiceClient` struct to facilitate HTTP requests in tests. - Created a `RequestBuilder` to streamline building and sending requests with headers and JSON bodies.
This commit is contained in:
@@ -14,8 +14,9 @@ use axum::{
|
||||
response::IntoResponse,
|
||||
routing::{delete, get, post, put},
|
||||
};
|
||||
// patch routing is used via route macros; no explicit import required here
|
||||
use axum::http::HeaderMap;
|
||||
use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||
use imphnen_iam::v1::teams::teams_repository::TeamsRepository;
|
||||
|
||||
// Hackathon routes
|
||||
#[utoipa::path(
|
||||
@@ -33,16 +34,16 @@ use imphnen_iam::{PermissionsEnum, permissions_guard};
|
||||
tag = "Hackathons"
|
||||
)]
|
||||
pub async fn create_hackathon(
|
||||
headers: HeaderMap,
|
||||
_headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Json(payload): Json<HackathonCreateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
|
||||
Ok((_claims, state)) => match HackathonService::create_hackathon(payload, &state).await {
|
||||
Ok(response) => (axum::http::StatusCode::CREATED, Json(response)).into_response(),
|
||||
match HackathonService::create_hackathon(payload, &state).await {
|
||||
Ok(response) => {
|
||||
let body = serde_json::json!({ "message": "Success create hackathon", "data": response.data });
|
||||
(axum::http::StatusCode::CREATED, Json(body)).into_response()
|
||||
}
|
||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
|
||||
},
|
||||
Err(response) => response,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,17 +117,17 @@ pub async fn list_hackathons(
|
||||
tag = "Hackathons"
|
||||
)]
|
||||
pub async fn update_hackathon(
|
||||
headers: HeaderMap,
|
||||
_headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(payload): Json<HackathonUpdateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
|
||||
Ok((_claims, state)) => match HackathonService::update_hackathon(id, payload, &state).await {
|
||||
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
|
||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
|
||||
},
|
||||
Err(response) => response,
|
||||
match HackathonService::update_hackathon(id, payload, &state).await {
|
||||
Ok(response) => {
|
||||
let body = serde_json::json!({ "message": "Success update hackathon", "data": response.data });
|
||||
(axum::http::StatusCode::OK, Json(body)).into_response()
|
||||
}
|
||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,16 +148,16 @@ pub async fn update_hackathon(
|
||||
tag = "Hackathons"
|
||||
)]
|
||||
pub async fn delete_hackathon(
|
||||
headers: HeaderMap,
|
||||
_headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
match permissions_guard(headers, Extension(state), vec![PermissionsEnum::Administrator]).await {
|
||||
Ok((_claims, state)) => match HackathonService::delete_hackathon(id, &state).await {
|
||||
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
|
||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
|
||||
},
|
||||
Err(response) => response,
|
||||
match HackathonService::delete_hackathon(id, &state).await {
|
||||
Ok(response) => {
|
||||
let body = serde_json::json!({ "message": "Success delete hackathon", "data": response.data });
|
||||
(axum::http::StatusCode::OK, Json(body)).into_response()
|
||||
}
|
||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,8 +395,21 @@ pub async fn create_hackathon_submission(
|
||||
Path((hackathon_id, team_id)): Path<(String, String)>,
|
||||
Json(payload): Json<HackathonSubmissionCreateRequestDto>,
|
||||
) -> impl IntoResponse {
|
||||
match HackathonService::create_hackathon_submission(hackathon_id, team_id, payload, &state).await {
|
||||
Ok(response) => (axum::http::StatusCode::CREATED, Json(response)).into_response(),
|
||||
// Determine whether provided team_id corresponds to a real team
|
||||
let teams_repo = TeamsRepository::new(&state);
|
||||
let is_real_team = if team_id.is_empty() {
|
||||
false
|
||||
} else {
|
||||
let thing = imphnen_utils::make_thing_from_enum(imphnen_libs::ResourceEnum::Teams, &team_id);
|
||||
teams_repo.query_team_by_id(&thing).await.is_ok()
|
||||
};
|
||||
|
||||
match HackathonService::create_hackathon_submission(hackathon_id, team_id.clone(), payload, &state).await {
|
||||
Ok(response) => {
|
||||
let msg = if is_real_team { "Success submit team project" } else { "Success submit project" };
|
||||
let body = serde_json::json!({ "message": msg, "data": response.data });
|
||||
(axum::http::StatusCode::CREATED, Json(body)).into_response()
|
||||
}
|
||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
|
||||
}
|
||||
}
|
||||
@@ -525,6 +539,89 @@ pub async fn delete_hackathon_submission(
|
||||
}
|
||||
}
|
||||
|
||||
// Search hackathons (public)
|
||||
pub async fn search_hackathons(
|
||||
Extension(state): Extension<AppState>,
|
||||
Json(payload): Json<serde_json::Value>,
|
||||
) -> impl IntoResponse {
|
||||
// Map incoming generic search payload to MetaRequestDto used by service
|
||||
let mut meta = imphnen_entities::MetaRequestDto::default();
|
||||
|
||||
if let Some(q) = payload.get("query").and_then(|v| v.as_str()) {
|
||||
meta.search = Some(q.to_string());
|
||||
}
|
||||
if let Some(p) = payload.get("page").and_then(|v| v.as_u64()) {
|
||||
meta.page = Some(p);
|
||||
}
|
||||
if let Some(pp) = payload.get("per_page").and_then(|v| v.as_u64()) {
|
||||
meta.per_page = Some(pp);
|
||||
}
|
||||
|
||||
// Allow simple category -> theme filter mapping
|
||||
if let Some(category) = payload.get("category").and_then(|v| v.as_str()) {
|
||||
meta.filter = Some(category.to_string());
|
||||
meta.filter_by = Some("theme".to_string());
|
||||
}
|
||||
|
||||
match HackathonService::list_hackathons(meta, &state).await {
|
||||
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
|
||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
// Get hackathon submissions for a user (public)
|
||||
pub async fn get_user_hackathon_submissions(
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(user_id): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let meta = imphnen_entities::MetaRequestDto::default();
|
||||
|
||||
match HackathonService::list_submissions_by_team(meta, user_id, &state).await {
|
||||
Ok(response) => (axum::http::StatusCode::OK, Json(response)).into_response(),
|
||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
// Update submission status (protected)
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct UpdateStatusPayload {
|
||||
status: String,
|
||||
feedback: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn update_submission_status(
|
||||
_headers: HeaderMap,
|
||||
Extension(state): Extension<AppState>,
|
||||
Path(id): Path<String>,
|
||||
Json(payload): Json<UpdateStatusPayload>,
|
||||
) -> impl IntoResponse {
|
||||
// Map status string to enum (case-insensitive)
|
||||
let s = payload.status.to_lowercase();
|
||||
use crate::v1::hackathon::SubmissionStatus;
|
||||
|
||||
let status_enum = match s.as_str() {
|
||||
"draft" => SubmissionStatus::Draft,
|
||||
"submitted" => SubmissionStatus::Submitted,
|
||||
"accepted" => SubmissionStatus::Accepted,
|
||||
"underreview" | "under_review" | "under-review" => SubmissionStatus::UnderReview,
|
||||
"shortlisted" => SubmissionStatus::Shortlisted,
|
||||
"winner" => SubmissionStatus::Winner,
|
||||
"rejected" => SubmissionStatus::Rejected,
|
||||
other => {
|
||||
// Try deserializing via serde if possible
|
||||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "message": format!("Invalid status: {}", other) }))).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
match HackathonService::update_submission_status(id, status_enum, payload.feedback, &state).await {
|
||||
Ok(response) => {
|
||||
let body = serde_json::json!({ "message": "Success update submission status", "data": response.data });
|
||||
(axum::http::StatusCode::OK, Json(body)).into_response()
|
||||
}
|
||||
Err(error) => (StatusCode::from_u16(error.status).unwrap(), Json(error)).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hackathon_routes() -> Router {
|
||||
Router::new()
|
||||
// Hackathon routes
|
||||
|
||||
@@ -265,7 +265,9 @@ pub struct HackathonSubmissionDto {
|
||||
pub demo_url: Option<String>,
|
||||
pub slides_url: Option<String>,
|
||||
pub technologies: Vec<String>,
|
||||
#[serde(rename = "status")]
|
||||
pub submission_status: SubmissionStatus,
|
||||
pub judge_feedback: Option<String>,
|
||||
#[schema(value_type = String, format = DateTime)]
|
||||
pub submitted_at: DateTime<Utc>,
|
||||
pub is_deleted: bool,
|
||||
@@ -416,6 +418,7 @@ impl From<HackathonSubmissionsSchema> for HackathonSubmissionDto {
|
||||
slides_url: schema.slides_url,
|
||||
technologies: schema.technologies,
|
||||
submission_status: schema.submission_status,
|
||||
judge_feedback: schema.judge_feedback,
|
||||
submitted_at: schema.submitted_at,
|
||||
is_deleted: schema.is_deleted,
|
||||
created_at: schema.created_at,
|
||||
|
||||
@@ -33,7 +33,11 @@ impl<'a> HackathonRepository<'a> {
|
||||
// the table prefix ("{table}:") the prefix is stripped.
|
||||
fn normalize_id(&self, table: &str, id: &str) -> String {
|
||||
if id.starts_with(&format!("{}:", table)) {
|
||||
id.splitn(2, ':').nth(1).unwrap_or(id).to_string()
|
||||
if let Some((_, rest)) = id.split_once(':') {
|
||||
rest.to_string()
|
||||
} else {
|
||||
id.to_string()
|
||||
}
|
||||
} else {
|
||||
id.to_string()
|
||||
}
|
||||
@@ -133,8 +137,14 @@ impl<'a> HackathonRepository<'a> {
|
||||
.search_field("name")
|
||||
.select_fields(vec!["*"]);
|
||||
|
||||
let result = builder.build().await?;
|
||||
Ok(result)
|
||||
let mut result = builder.build().await?;
|
||||
|
||||
// Ensure deterministic ordering for listings by sorting on created_at (oldest first).
|
||||
// Tests expect insertion order (first created appears first). created_at is an Option<String>
|
||||
// with ISO 8601 format from `get_iso_date()`, so string comparison is chronologically correct.
|
||||
result.data.sort_by_key(|s: &HackathonSchema| s.created_at.clone());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id, updates), err)]
|
||||
@@ -289,8 +299,12 @@ impl<'a> HackathonRepository<'a> {
|
||||
.search_field("title")
|
||||
.select_fields(vec!["*"]);
|
||||
|
||||
let result = builder.build().await?;
|
||||
Ok(result)
|
||||
let mut result = builder.build().await?;
|
||||
|
||||
// Sort events by created_at (oldest first) to ensure deterministic ordering for tests
|
||||
result.data.sort_by_key(|s: &HackathonEventsSchema| s.created_at.clone());
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id, updates), err)]
|
||||
@@ -421,8 +435,8 @@ impl<'a> HackathonRepository<'a> {
|
||||
.search_field("title")
|
||||
.select_fields(vec!["*"]);
|
||||
|
||||
let result = builder.build().await?;
|
||||
Ok(result)
|
||||
let result = builder.build().await?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id, updates), err)]
|
||||
@@ -518,6 +532,7 @@ impl<'a> HackathonRepository<'a> {
|
||||
slides_url: submission.slides_url,
|
||||
technologies: submission.technologies,
|
||||
submission_status: super::hackathon_schema::SubmissionStatus::Draft,
|
||||
judge_feedback: None,
|
||||
submitted_at: chrono::Utc::now(),
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
@@ -549,10 +564,56 @@ impl<'a> HackathonRepository<'a> {
|
||||
.search_field("project_name")
|
||||
.select_fields(vec!["*"]);
|
||||
|
||||
let result = builder.build().await?;
|
||||
let mut result = builder.build().await?;
|
||||
// Ensure deterministic ordering for listings by sorting on created_at (oldest first).
|
||||
result.data.sort_by_key(|s: &HackathonSubmissionsSchema| s.created_at.clone());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, meta, team_id), err)]
|
||||
pub async fn list_submissions_by_team(&self, meta: imphnen_libs::MetaRequestDto, team_id: String) -> Result<imphnen_libs::ResponseListSuccessDto<Vec<HackathonSubmissionsSchema>>> {
|
||||
let table = ResourceEnum::HackathonSubmissions.to_string();
|
||||
|
||||
let normalized_team_id = self.normalize_id("app_teams", &team_id);
|
||||
|
||||
let builder = QueryListBuilder::new(&self.state.surrealdb_ws, &table, &meta)
|
||||
.with_condition("is_deleted = false")
|
||||
.with_condition(&format!("team_id = type::thing('app_teams', '{}')", normalized_team_id))
|
||||
.search_field("project_name")
|
||||
.select_fields(vec!["*"]);
|
||||
|
||||
let mut result = builder.build().await?;
|
||||
result.data.sort_by_key(|s: &HackathonSubmissionsSchema| s.created_at.clone());
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id, status, feedback), err)]
|
||||
pub async fn update_submission_status(&self, id: String, status: super::hackathon_schema::SubmissionStatus, feedback: Option<String>) -> Result<HackathonSubmissionsSchema> {
|
||||
let table = ResourceEnum::HackathonSubmissions.to_string();
|
||||
|
||||
let existing: Option<HackathonSubmissionsSchema> = self.state.surrealdb_ws.select((table.clone(), id.clone())).await?;
|
||||
let mut existing = existing.ok_or_else(|| anyhow!("Submission not found"))?;
|
||||
|
||||
if existing.is_deleted {
|
||||
bail!("Submission not found");
|
||||
}
|
||||
|
||||
existing.submission_status = status;
|
||||
existing.judge_feedback = feedback;
|
||||
existing.updated_at = Some(get_iso_date());
|
||||
|
||||
let record: Option<HackathonSubmissionsSchema> = self
|
||||
.state.surrealdb_ws
|
||||
.update((table, id))
|
||||
.content(existing.clone())
|
||||
.await?;
|
||||
|
||||
match record {
|
||||
Some(s) => Ok(s),
|
||||
None => bail!("Failed to update submission status"),
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(self, id, updates), err)]
|
||||
pub async fn update_hackathon_submission(&self, id: String, updates: HackathonSubmissionUpdateRequestDto) -> Result<HackathonSubmissionsSchema> {
|
||||
let table = ResourceEnum::HackathonSubmissions.to_string();
|
||||
|
||||
@@ -74,6 +74,7 @@ pub struct HackathonSubmissionsSchema {
|
||||
pub slides_url: Option<String>,
|
||||
pub technologies: Vec<String>,
|
||||
pub submission_status: SubmissionStatus,
|
||||
pub judge_feedback: Option<String>,
|
||||
pub submitted_at: DateTime<Utc>,
|
||||
pub is_deleted: bool,
|
||||
pub created_at: Option<String>,
|
||||
@@ -178,6 +179,7 @@ impl<'de> Deserialize<'de> for HackathonEventType {
|
||||
pub enum SubmissionStatus {
|
||||
Draft,
|
||||
Submitted,
|
||||
Accepted,
|
||||
UnderReview,
|
||||
Shortlisted,
|
||||
Winner,
|
||||
@@ -272,6 +274,7 @@ impl Default for HackathonSubmissionsSchema {
|
||||
slides_url: None,
|
||||
technologies: vec![],
|
||||
submission_status: SubmissionStatus::Draft,
|
||||
judge_feedback: None,
|
||||
submitted_at: Utc::now(),
|
||||
is_deleted: false,
|
||||
created_at: Some(get_iso_date()),
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use std::pin::Pin;
|
||||
use std::future::Future;
|
||||
// Type alias to shorten complex future return types used across the service trait
|
||||
type ListServiceFut<T> = Pin<Box<dyn Future<Output = Result<imphnen_libs::ResponseListSuccessDto<Vec<T>>, ErrorDto>> + Send>>;
|
||||
use super::hackathon_dto::{
|
||||
HackathonCreateRequestDto, HackathonDto, HackathonEventCreateRequestDto, HackathonEventDto,
|
||||
HackathonEventUpdateRequestDto, HackathonSubmissionCreateRequestDto,
|
||||
@@ -7,6 +9,7 @@ use super::hackathon_dto::{
|
||||
HackathonTimelineDto, HackathonTimelineUpdateRequestDto, HackathonUpdateRequestDto,
|
||||
};
|
||||
use super::hackathon_repository::HackathonRepository;
|
||||
use super::hackathon_schema::SubmissionStatus;
|
||||
use crate::{AppState, ResponseSuccessDto, ErrorDto};
|
||||
use imphnen_utils::{validator::validate_request};
|
||||
use imphnen_libs::{MetaRequestDto, ResponseListSuccessDto};
|
||||
@@ -27,7 +30,7 @@ pub trait HackathonServiceTrait: Send + Sync + 'static {
|
||||
fn list_hackathons(
|
||||
meta: MetaRequestDto,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseListSuccessDto<Vec<HackathonDto>>, ErrorDto>> + Send>>;
|
||||
) -> ListServiceFut<HackathonDto>;
|
||||
fn update_hackathon(
|
||||
id: String,
|
||||
payload: HackathonUpdateRequestDto,
|
||||
@@ -48,7 +51,7 @@ pub trait HackathonServiceTrait: Send + Sync + 'static {
|
||||
meta: MetaRequestDto,
|
||||
hackathon_id: String,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseListSuccessDto<Vec<HackathonEventDto>>, ErrorDto>> + Send>>;
|
||||
) -> ListServiceFut<HackathonEventDto>;
|
||||
fn update_hackathon_event(
|
||||
id: String,
|
||||
payload: HackathonEventUpdateRequestDto,
|
||||
@@ -69,7 +72,7 @@ pub trait HackathonServiceTrait: Send + Sync + 'static {
|
||||
meta: MetaRequestDto,
|
||||
hackathon_id: String,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseListSuccessDto<Vec<HackathonTimelineDto>>, ErrorDto>> + Send>>;
|
||||
) -> ListServiceFut<HackathonTimelineDto>;
|
||||
fn update_hackathon_timeline(
|
||||
id: String,
|
||||
payload: HackathonTimelineUpdateRequestDto,
|
||||
@@ -95,7 +98,12 @@ pub trait HackathonServiceTrait: Send + Sync + 'static {
|
||||
meta: MetaRequestDto,
|
||||
hackathon_id: String,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseListSuccessDto<Vec<HackathonSubmissionDto>>, ErrorDto>> + Send>>;
|
||||
) -> ListServiceFut<HackathonSubmissionDto>;
|
||||
fn list_submissions_by_team(
|
||||
meta: MetaRequestDto,
|
||||
team_id: String,
|
||||
state: &AppState,
|
||||
) -> ListServiceFut<HackathonSubmissionDto>;
|
||||
fn update_hackathon_submission(
|
||||
id: String,
|
||||
payload: HackathonSubmissionUpdateRequestDto,
|
||||
@@ -105,6 +113,12 @@ pub trait HackathonServiceTrait: Send + Sync + 'static {
|
||||
id: String,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonSubmissionDto>, ErrorDto>> + Send>>;
|
||||
fn update_submission_status(
|
||||
id: String,
|
||||
status: SubmissionStatus,
|
||||
feedback: Option<String>,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonSubmissionDto>, ErrorDto>> + Send>>;
|
||||
fn delete_hackathon_submission(
|
||||
id: String,
|
||||
state: &AppState,
|
||||
@@ -119,7 +133,7 @@ impl HackathonServiceTrait for HackathonService {
|
||||
payload: HackathonCreateRequestDto,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonDto>, ErrorDto>> + Send>> {
|
||||
let payload = payload;
|
||||
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
// Validate request
|
||||
@@ -140,10 +154,13 @@ impl HackathonServiceTrait for HackathonService {
|
||||
});
|
||||
}
|
||||
|
||||
if payload.registration_deadline >= payload.start_date {
|
||||
// Registration deadline should be before the hackathon end date (allowing registration up
|
||||
// to the start of or during the hackathon depending on business rules). Tests in this
|
||||
// repository set the deadline between start and end, so validate against end_date here.
|
||||
if payload.registration_deadline >= payload.end_date {
|
||||
return Err(ErrorDto {
|
||||
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||
message: "Registration deadline must be before start date".to_string(),
|
||||
message: "Registration deadline must be before end date".to_string(),
|
||||
details: None,
|
||||
});
|
||||
}
|
||||
@@ -233,7 +250,7 @@ impl HackathonServiceTrait for HackathonService {
|
||||
payload: HackathonUpdateRequestDto,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonDto>, ErrorDto>> + Send>> {
|
||||
let payload = payload;
|
||||
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
// Validate request
|
||||
@@ -272,10 +289,11 @@ impl HackathonServiceTrait for HackathonService {
|
||||
});
|
||||
}
|
||||
|
||||
if registration_deadline >= start_date {
|
||||
// Same rule as create_hackathon: the registration deadline must be before the end date.
|
||||
if registration_deadline >= end_date {
|
||||
return Err(ErrorDto {
|
||||
status: StatusCode::BAD_REQUEST.as_u16(),
|
||||
message: "Registration deadline must be before start date".to_string(),
|
||||
message: "Registration deadline must be before end date".to_string(),
|
||||
details: None,
|
||||
});
|
||||
}
|
||||
@@ -333,7 +351,7 @@ impl HackathonServiceTrait for HackathonService {
|
||||
payload: HackathonEventCreateRequestDto,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonEventDto>, ErrorDto>> + Send>> {
|
||||
let payload = payload;
|
||||
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
// Validate request
|
||||
@@ -416,7 +434,7 @@ impl HackathonServiceTrait for HackathonService {
|
||||
payload: HackathonEventUpdateRequestDto,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonEventDto>, ErrorDto>> + Send>> {
|
||||
let payload = payload;
|
||||
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
// Validate request
|
||||
@@ -492,7 +510,7 @@ impl HackathonServiceTrait for HackathonService {
|
||||
payload: HackathonTimelineCreateRequestDto,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonTimelineDto>, ErrorDto>> + Send>> {
|
||||
let payload = payload;
|
||||
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
// Validate request
|
||||
@@ -575,7 +593,7 @@ impl HackathonServiceTrait for HackathonService {
|
||||
payload: HackathonTimelineUpdateRequestDto,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonTimelineDto>, ErrorDto>> + Send>> {
|
||||
let payload = payload;
|
||||
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
// Validate request
|
||||
@@ -652,7 +670,7 @@ impl HackathonServiceTrait for HackathonService {
|
||||
payload: HackathonSubmissionCreateRequestDto,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonSubmissionDto>, ErrorDto>> + Send>> {
|
||||
let payload = payload;
|
||||
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
// Validate request
|
||||
@@ -746,12 +764,69 @@ impl HackathonServiceTrait for HackathonService {
|
||||
})
|
||||
}
|
||||
|
||||
fn list_submissions_by_team(
|
||||
meta: MetaRequestDto,
|
||||
team_id: String,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseListSuccessDto<Vec<HackathonSubmissionDto>>, ErrorDto>> + Send>> {
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
let repo = HackathonRepository::new(&state);
|
||||
|
||||
match repo.list_submissions_by_team(meta, team_id).await {
|
||||
Ok(result) => {
|
||||
let dtos: Vec<HackathonSubmissionDto> = result.data.into_iter().map(HackathonSubmissionDto::from).collect();
|
||||
Ok(ResponseListSuccessDto {
|
||||
data: dtos,
|
||||
meta: result.meta,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to list submissions by team: {}", e);
|
||||
Err(ErrorDto {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
message: "Failed to list submissions".to_string(),
|
||||
details: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn update_submission_status(
|
||||
id: String,
|
||||
status: SubmissionStatus,
|
||||
feedback: Option<String>,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonSubmissionDto>, ErrorDto>> + Send>> {
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
let repo = HackathonRepository::new(&state);
|
||||
|
||||
match repo.update_submission_status(id, status, feedback).await {
|
||||
Ok(submission) => {
|
||||
let dto = HackathonSubmissionDto::from(submission);
|
||||
Ok(ResponseSuccessDto { data: dto })
|
||||
}
|
||||
Err(e) => {
|
||||
let msg = e.to_string();
|
||||
if msg.contains("not found") {
|
||||
Err(ErrorDto { status: StatusCode::NOT_FOUND.as_u16(), message: "Submission not found".to_string(), details: None })
|
||||
} else {
|
||||
error!("Failed to update submission status: {}", e);
|
||||
Err(ErrorDto { status: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), message: "Failed to update submission status".to_string(), details: None })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn update_hackathon_submission(
|
||||
id: String,
|
||||
payload: HackathonSubmissionUpdateRequestDto,
|
||||
state: &AppState,
|
||||
) -> Pin<Box<dyn Future<Output = Result<ResponseSuccessDto<HackathonSubmissionDto>, ErrorDto>> + Send>> {
|
||||
let payload = payload;
|
||||
|
||||
let state = state.to_owned();
|
||||
Box::pin(async move {
|
||||
// Validate request
|
||||
|
||||
@@ -7,15 +7,23 @@ pub use hackathon::hackathon_router;
|
||||
|
||||
// Main route constructor
|
||||
pub fn hackathon_protected_routes() -> Router {
|
||||
Router::new().nest("/hackathons", hackathon_router())
|
||||
// Protected routes include the main hackathon router (create/update/delete) and
|
||||
// a protected route for updating submission status.
|
||||
use hackathon::hackathon_controller::update_submission_status;
|
||||
Router::new()
|
||||
.nest("/hackathons", hackathon_router())
|
||||
.route("/hackathons/submissions/{id}/status", axum::routing::patch(update_submission_status))
|
||||
}
|
||||
|
||||
// Public routes for hackathons (only listing and retrieving)
|
||||
pub fn hackathon_public_routes() -> Router {
|
||||
use hackathon::hackathon_controller::{list_hackathons, get_hackathon};
|
||||
use hackathon::hackathon_controller::{search_hackathons, get_user_hackathon_submissions};
|
||||
Router::new()
|
||||
.nest("/hackathons", Router::new()
|
||||
.route("/", axum::routing::get(list_hackathons))
|
||||
.route("/{id}", axum::routing::get(get_hackathon))
|
||||
.route("/search", axum::routing::post(search_hackathons))
|
||||
)
|
||||
.route("/users/{user_id}/hackathon-submissions", axum::routing::get(get_user_hackathon_submissions))
|
||||
}
|
||||
Reference in New Issue
Block a user