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:
MythEclipse
2025-10-11 10:58:51 +07:00
parent 466ba3391a
commit c10443f881
33 changed files with 4547 additions and 4082 deletions
@@ -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
+9 -1
View File
@@ -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))
}