From 4c435708c04488f1d5a4f6d0cf37dda7147e1455 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Sat, 27 Sep 2025 19:24:45 +0700 Subject: [PATCH] feat: Add endpoint to retrieve hackathon submissions and implement seeding for test submissions --- .../src/bin/seed_test_submission.rs | 359 ++++++++++++++++++ .../src/v1/hackathon/hackathon_controller.rs | 24 ++ .../src/v1/hackathon/hackathon_service.rs | 29 ++ test.sh | 73 ++++ 4 files changed, 485 insertions(+) create mode 100644 imphnen-backend/src/bin/seed_test_submission.rs diff --git a/imphnen-backend/src/bin/seed_test_submission.rs b/imphnen-backend/src/bin/seed_test_submission.rs new file mode 100644 index 0000000..57fd438 --- /dev/null +++ b/imphnen-backend/src/bin/seed_test_submission.rs @@ -0,0 +1,359 @@ +use chrono::{DateTime, Utc}; +use imphnen_hackathon::v1::hackathon::hackathon_schema::{ + HackathonSchema, HackathonEventsSchema, HackathonTimelineSchema, HackathonSubmissionsSchema, + HackathonStatus, HackathonEventType, HackathonPhase, SubmissionStatus, Prize +}; +use imphnen_iam::{UsersSchema, v1::teams::TeamsSchema}; +use imphnen_utils::{get_iso_date, hash_password}; +use std::error::Error; +use surrealdb::{opt::auth::Root, sql::Thing}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let env = &imphnen_libs::environment::ENV; + use surrealdb::engine::any; + let db = any::connect(&env.surrealdb_url).await?; + db.signin(Root { + username: &env.surrealdb_username, + password: &env.surrealdb_password, + }) + .await?; + db.use_ns(env.surrealdb_namespace.clone()) + .use_db(env.surrealdb_dbname.clone()) + .await?; + + // Test users for submission testing + let test_users = vec![ + ( + "test-user-001", + "testuser1@example.com", + "Test User 1", + "5713cb37-dc02-4e87-8048-d7a41d352059", // User role + ), + ( + "test-user-002", + "testuser2@example.com", + "Test User 2", + "5713cb37-dc02-4e87-8048-d7a41d352059", + ), + ( + "test-user-003", + "testuser3@example.com", + "Test User 3", + "5713cb37-dc02-4e87-8048-d7a41d352059", + ), + ]; + + // Test teams for submission testing + let test_teams = vec![ + ( + "test-team-001", + "Test Team Alpha", + Some("Team for testing hackathon submissions".to_string()), + "test-user-001", // team leader + true, + Some(5), + Some(vec!["JavaScript".to_string(), "React".to_string()]), + Some("Remote".to_string()), + ), + ( + "test-team-002", + "Test Team Beta", + Some("Another team for testing submissions".to_string()), + "test-user-002", + true, + Some(4), + Some(vec!["Python".to_string(), "Django".to_string()]), + Some("Remote".to_string()), + ), + ]; + + // Test hackathon for submission testing + let test_hackathons = vec![ + ( + "test-hackathon-001", + "Test Hackathon 2025", + "Hackathon for testing submission functionality.", + "2025-12-01T09:00:00Z", + "2025-12-03T18:00:00Z", + "2025-11-25T23:59:59Z", + Some(50), + HackathonStatus::RegistrationOpen, + Some("Testing & Development".to_string()), + Some("1. Test all submission features\n2. Teams can have 2-5 members\n3. Submit by deadline".to_string()), + Some(vec![ + Prize { position: 1, title: "Test Winner".to_string(), description: Some("Best test submission".to_string()), value: Some("$1,000".to_string()) }, + Prize { position: 2, title: "Test Runner-up".to_string(), description: Some("Second best submission".to_string()), value: Some("$500".to_string()) }, + ]), + vec!["c3b1d6a8-8d4f-4b36-b789-2e532ec7a7b2".to_string()], // admin user + ), + ]; + + // Test hackathon timeline + let test_timeline = vec![ + ( + "test-hackathon-001", + HackathonPhase::Registration, + "Registration Phase", + Some("Register your team for the test hackathon".to_string()), + "2025-11-20T00:00:00Z", + "2025-11-25T23:59:59Z", + true, + 1, + ), + ( + "test-hackathon-001", + HackathonPhase::Development, + "Development Phase", + Some("Build your test project".to_string()), + "2025-12-01T00:00:00Z", + "2025-12-02T23:59:59Z", + false, + 2, + ), + ( + "test-hackathon-001", + HackathonPhase::Submission, + "Submission Phase", + Some("Submit your test project".to_string()), + "2025-12-03T00:00:00Z", + "2025-12-03T12:00:00Z", + false, + 3, + ), + ]; + + // Test submissions + let test_submissions = vec![ + ( + "test-hackathon-001", + "test-team-001", + "Test Project Alpha", + "A comprehensive test project demonstrating all features.", + Some("https://github.com/test-team-alpha/test-project".to_string()), + Some("https://demo.test-project-alpha.com".to_string()), + Some("https://slides.test-project-alpha.com".to_string()), + vec!["JavaScript".to_string(), "React".to_string(), "Node.js".to_string()], + SubmissionStatus::Draft, + "2025-12-02T10:00:00Z", + ), + ( + "test-hackathon-001", + "test-team-002", + "Test Project Beta", + "Another test project with different technologies.", + Some("https://github.com/test-team-beta/test-project".to_string()), + Some("https://demo.test-project-beta.com".to_string()), + None, + vec!["Python".to_string(), "Django".to_string(), "PostgreSQL".to_string()], + SubmissionStatus::Submitted, + "2025-12-03T09:30:00Z", + ), + ]; + + // Seed test users + for (id, email, fullname, role_id) in test_users { + db.query("DELETE type::thing('app_users', $id)") + .bind(("id", id)) + .await?; + + let user = UsersSchema { + id: Thing::from(("app_users", id)), + fullname: fullname.into(), + legal_name: Some(format!("{} Legal Name", fullname)), + email: email.into(), + password: hash_password("password").unwrap(), + avatar: Some("https://example.com/avatar.jpg".into()), + phone_number: "081234567890".into(), + phone_for_verification: Some("081234567890".into()), + is_active: true, + is_deleted: false, + mentor_id: None, + gender: Some("male".into()), + birthdate: Some("1995-01-01".into()), + domicile: Some("Jakarta, Indonesia".into()), + bio: Some(format!("{} is a test user for hackathon submissions.", fullname)), + last_education: Some("S1 Computer Science".into()), + linkedin_url: Some("https://linkedin.com/in/testuser".into()), + github_url: Some("https://github.com/testuser".into()), + cv_url: Some("https://example.com/cv.pdf".into()), + portfolio_url: Some("https://example.com/portfolio".into()), + website_url: Some("https://example.com/website".into()), + twitter_url: Some("https://twitter.com/testuser".into()), + location: Some("Jakarta, Indonesia".into()), + skills: Some(vec!["JavaScript".to_string(), "Python".to_string()]), + experience: None, + education: None, + career_status: Some("Developer".into()), + role: Thing::from(("app_roles", role_id)), + created_at: get_iso_date(), + updated_at: get_iso_date(), + }; + + db.create::>(("app_users", id)) + .content(user) + .await?; + + println!("✅ Inserted test user: {fullname} ({email})"); + } + + // Seed test teams + for (id, name, description, leader_id, is_open, max_members, skills_required, location) in test_teams { + db.query("DELETE type::thing('app_teams', $id)") + .bind(("id", id)) + .await?; + + let team = TeamsSchema { + id: Thing::from(("app_teams", id)), + name: name.into(), + description, + leader_id: Thing::from(("app_users", leader_id)), + is_open, + max_members, + skills_required, + location, + avatar: None, + website_url: None, + github_url: None, + is_active: true, + is_deleted: false, + created_at: get_iso_date(), + updated_at: get_iso_date(), + }; + + db.create::>(("app_teams", id)) + .content(team) + .await?; + + println!("✅ Inserted test team: {name}"); + } + + // Seed test hackathons + for ( + id, + name, + description, + start_date, + end_date, + registration_deadline, + max_participants, + status, + theme, + rules, + prizes, + organizers, + ) in test_hackathons { + db.query("DELETE type::thing('app_hackathons', $id)") + .bind(("id", id)) + .await?; + + let hackathon = HackathonSchema { + id: Thing::from(("app_hackathons", id)), + name: name.into(), + description: description.into(), + start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc), + end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc), + registration_deadline: DateTime::parse_from_rfc3339(registration_deadline)?.with_timezone(&Utc), + max_participants, + status, + theme, + rules, + prizes, + organizers, + is_deleted: false, + created_at: Some(get_iso_date()), + updated_at: Some(get_iso_date()), + }; + + db.create::>(("app_hackathons", id)) + .content(hackathon) + .await?; + + println!("✅ Inserted test hackathon: {name}"); + } + + // Seed test hackathon timeline + for ( + hackathon_id, + phase, + title, + description, + start_date, + end_date, + is_active, + order, + ) in test_timeline { + let timeline_id = format!("test-timeline-{}-{}", hackathon_id, order); + + db.query("DELETE type::thing('app_hackathon_timeline', $id)") + .bind(("id", timeline_id.clone())) + .await?; + + let timeline = HackathonTimelineSchema { + id: Thing::from(("app_hackathon_timeline", timeline_id.as_str())), + hackathon_id: Thing::from(("app_hackathons", hackathon_id)), + phase, + title: title.into(), + description, + start_date: DateTime::parse_from_rfc3339(start_date)?.with_timezone(&Utc), + end_date: DateTime::parse_from_rfc3339(end_date)?.with_timezone(&Utc), + is_active, + order, + is_deleted: false, + created_at: Some(get_iso_date()), + updated_at: Some(get_iso_date()), + }; + + db.create::>(("app_hackathon_timeline", timeline_id)) + .content(timeline) + .await?; + + println!("✅ Inserted test hackathon timeline: {title}"); + } + + // Seed test hackathon submissions + for ( + hackathon_id, + team_id, + project_name, + description, + repository_url, + demo_url, + slides_url, + technologies, + submission_status, + submitted_at, + ) in test_submissions { + let submission_id = format!("test-submission-{}-{}", hackathon_id, team_id); + + db.query("DELETE type::thing('app_hackathon_submissions', $id)") + .bind(("id", submission_id.clone())) + .await?; + + let submission = HackathonSubmissionsSchema { + id: Thing::from(("app_hackathon_submissions", submission_id.as_str())), + hackathon_id: Thing::from(("app_hackathons", hackathon_id)), + team_id: Thing::from(("app_teams", team_id)), + project_name: project_name.into(), + description: description.into(), + repository_url, + demo_url, + slides_url, + technologies, + submission_status, + submitted_at: DateTime::parse_from_rfc3339(submitted_at)?.with_timezone(&Utc), + is_deleted: false, + created_at: Some(get_iso_date()), + updated_at: Some(get_iso_date()), + }; + + db.create::>(("app_hackathon_submissions", submission_id)) + .content(submission) + .await?; + + println!("✅ Inserted test hackathon submission: {project_name}"); + } + + println!("✅ All test submission data seeded"); + Ok(()) +} \ No newline at end of file diff --git a/imphnen-hackathon/src/v1/hackathon/hackathon_controller.rs b/imphnen-hackathon/src/v1/hackathon/hackathon_controller.rs index 8d9fe8b..5e6f2e2 100644 --- a/imphnen-hackathon/src/v1/hackathon/hackathon_controller.rs +++ b/imphnen-hackathon/src/v1/hackathon/hackathon_controller.rs @@ -407,6 +407,29 @@ pub async fn list_hackathon_submissions( } } +#[utoipa::path( + get, + path = "/v1/hackathons/submissions/{id}", + params( + ("id" = String, Path, description = "Submission ID") + ), + responses( + (status = 200, description = "Submission retrieved successfully", body = ResponseSuccessDto), + (status = 404, description = "Submission not found", body = ErrorDto), + (status = 500, description = "Internal server error", body = ErrorDto) + ), + tag = "Hackathon Submissions" +)] +pub async fn get_hackathon_submission( + Extension(state): Extension, + Path(id): Path, +) -> impl IntoResponse { + match HackathonService::get_hackathon_submission(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(), + } +} + #[utoipa::path( put, path = "/v1/hackathons/submissions/{id}", @@ -503,6 +526,7 @@ pub fn hackathon_routes() -> Router { // Hackathon Submissions routes .route("/{hackathon_id}/teams/{team_id}/submissions", post(create_hackathon_submission)) .route("/{hackathon_id}/submissions", get(list_hackathon_submissions)) + .route("/submissions/{id}", get(get_hackathon_submission)) .route("/submissions/{id}", put(update_hackathon_submission)) .route("/submissions/{id}/submit", post(submit_hackathon_submission)) .route("/submissions/{id}", delete(delete_hackathon_submission)) diff --git a/imphnen-hackathon/src/v1/hackathon/hackathon_service.rs b/imphnen-hackathon/src/v1/hackathon/hackathon_service.rs index b90aad8..4adf686 100644 --- a/imphnen-hackathon/src/v1/hackathon/hackathon_service.rs +++ b/imphnen-hackathon/src/v1/hackathon/hackathon_service.rs @@ -87,6 +87,10 @@ pub trait HackathonServiceTrait: Send + Sync + 'static { payload: HackathonSubmissionCreateRequestDto, state: &AppState, ) -> Pin, ErrorDto>> + Send>>; + fn get_hackathon_submission( + id: String, + state: &AppState, + ) -> Pin, ErrorDto>> + Send>>; fn list_hackathon_submissions( meta: MetaRequestDto, hackathon_id: String, @@ -688,6 +692,31 @@ impl HackathonServiceTrait for HackathonService { }) } + fn get_hackathon_submission( + id: String, + state: &AppState, + ) -> Pin, ErrorDto>> + Send>> { + let state = state.to_owned(); + Box::pin(async move { + let repo = HackathonRepository::new(&state); + + match repo.get_hackathon_submission_by_id(id).await { + Ok(submission) => { + let dto = HackathonSubmissionDto::from(submission); + Ok(ResponseSuccessDto { data: dto }) + } + Err(e) => { + error!("Failed to get hackathon submission: {}", e); + Err(ErrorDto { + status: StatusCode::NOT_FOUND.as_u16(), + message: "Submission not found".to_string(), + details: None, + }) + } + } + }) + } + fn list_hackathon_submissions( meta: MetaRequestDto, hackathon_id: String, diff --git a/test.sh b/test.sh index 3a6fa24..3fd3311 100644 --- a/test.sh +++ b/test.sh @@ -12,6 +12,9 @@ declare -A ALL_USERS=( ["admin@example.com"]="Admin" ["staff@example.com"]="Staff" ["user@example.com"]="User" + ["testuser1@example.com"]="Test User 1" + ["testuser2@example.com"]="Test User 2" + ["testuser3@example.com"]="Test User 3" ["mentor@example.com"]="Mentor User" ) @@ -840,6 +843,65 @@ test_team_endpoints() { fi } +test_hackathon_endpoints() { + printf "\n${CYAN}=== Menguji Hackathon Endpoints ===${NC}\n" + test_api_endpoint "Get Hackathons List" "GET" "/v1/hackathons" 200 "" true + test_api_endpoint "Get Hackathons with Pagination" "GET" "/v1/hackathons?page=1&per_page=5" 200 "" true + test_api_endpoint "Search Hackathons" "GET" "/v1/hackathons?search=test" 200 "" true + + # Test specific hackathon by ID (assuming test hackathon exists from seeder) + local test_hackathon_id="test-hackathon-001" + test_api_endpoint "Get Hackathon By ID" "GET" "/v1/hackathons/$test_hackathon_id" 200 "" true + + # Test submission endpoints + test_api_endpoint "Get Submissions List" "GET" "/v1/hackathons/$test_hackathon_id/submissions" 200 "" true + test_api_endpoint "Get Submissions with Pagination" "GET" "/v1/hackathons/$test_hackathon_id/submissions?page=1&per_page=5" 200 "" true + + # Test creating a submission (assuming test user is logged in) + local submission_data + submission_data=$(jq -n --arg hackathon_id "$test_hackathon_id" --arg team_id "test-team-001" '{ + hackathon_id: $hackathon_id, + team_id: $team_id, + project_name: "Test Project Submission", + description: "This is a test project submission for hackathon testing", + technologies: ["Rust", "React", "PostgreSQL"], + repository_url: "https://github.com/test/test-project", + demo_url: "https://demo.test.com", + presentation_url: "https://slides.test.com" + }') + local create_response + create_response=$(test_api_endpoint "Create Hackathon Submission" "POST" "/v1/hackathons/$test_hackathon_id/teams/test-team-001/submissions" 201 "$submission_data" true) + local test_submission_id=$(echo "$create_response" | jq -r '.data.id // empty') + + # Test getting submission by ID + if [ -n "$test_submission_id" ]; then + test_api_endpoint "Get Submission By ID" "GET" "/v1/hackathons/submissions/$test_submission_id" 200 "" true + else + write_test_log "WARN" "✗ Get Submission By ID - Dilewati: Failed to capture submission ID from creation response" + fi + + # Test updating submission + if [ -n "$test_submission_id" ]; then + local update_submission_data + update_submission_data=$(jq -n --arg hackathon_id "$test_hackathon_id" --arg team_id "test-team-001" '{ + hackathon_id: $hackathon_id, + team_id: $team_id, + project_name: "Updated Test Project Submission", + description: "This is an updated test project submission for hackathon testing", + technologies: ["Rust", "React", "PostgreSQL", "Docker"], + repository_url: "https://github.com/test/updated-test-project", + demo_url: "https://updated-demo.test.com", + presentation_url: "https://updated-slides.test.com" + }') + test_api_endpoint "Update Hackathon Submission" "PUT" "/v1/hackathons/submissions/$test_submission_id" 200 "$update_submission_data" true + + # Test deleting submission + test_api_endpoint "Delete Hackathon Submission" "DELETE" "/v1/hackathons/submissions/$test_submission_id" 200 "" true + else + write_test_log "WARN" "✗ Update/Delete Submission tests - Dilewati: Failed to capture submission ID from creation response" + fi +} + test_advanced_scenarios() { printf "\n${CYAN}=== Menguji Advanced Scenarios ===${NC}\n" @@ -952,12 +1014,22 @@ if [ "$SKIP_SEED" = true ]; then exit 1 fi write_test_log "SUCCESS" "Gacha rolls seeded." + if ! RUST_LOG=debug cargo run --bin seed_test_submission; then + write_test_log "ERROR" "Gagal menjalankan seed test submission." + exit 1 + fi + write_test_log "SUCCESS" "Test submission seeded." else if ! RUST_LOG=debug cargo run --bin seeder; then write_test_log "ERROR" "Gagal menjalankan seeder roles permissions." exit 1 fi write_test_log "SUCCESS" "Seeders selesai." + if ! RUST_LOG=debug cargo run --bin seed_test_submission; then + write_test_log "ERROR" "Gagal menjalankan seed test submission." + exit 1 + fi + write_test_log "SUCCESS" "Test submission seeded." fi @@ -996,6 +1068,7 @@ if [[ "$SKIP_COMPREHENSIVE" = false && -n "$AUTH_TOKEN" ]]; then test_testimonials_endpoints # This will now use TEST_TESTIMONIAL_ID test_gacha_endpoints test_team_endpoints + test_hackathon_endpoints fi test_advanced_scenarios