diff --git a/backend/src/services/scheduler.rs b/backend/src/services/scheduler.rs index 0983d3a..74e1b6c 100644 --- a/backend/src/services/scheduler.rs +++ b/backend/src/services/scheduler.rs @@ -90,3 +90,14 @@ pub async fn run_scheduled_jobs(state: &AppState) { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn current_day_code_returns_valid_code() { + let code = current_day_code(); + assert!(["mon", "tue", "wed", "thu", "fri", "sat", "sun"].contains(&code)); + } +} diff --git a/backend/tests/api_schedules_test.rs b/backend/tests/api_schedules_test.rs index 44aa9a2..37f7bde 100644 --- a/backend/tests/api_schedules_test.rs +++ b/backend/tests/api_schedules_test.rs @@ -12,6 +12,7 @@ mod common; use axum::http::StatusCode; +use ai_synth_backend::db; fn require_test_db() -> bool { std::env::var("TEST_DATABASE_URL").is_ok() @@ -400,3 +401,217 @@ async fn schedule_requires_theme_ownership() { "User B should get 404 when attempting to PUT schedule on User A's theme" ); } + +// ═══════════════════════════════════════════════════════════════════════════ +// find_due_schedules — DB query logic +// ═══════════════════════════════════════════════════════════════════════════ + +/// Return (day_code, time_utc) for the current UTC moment. +fn today_day_and_time() -> (String, String) { + use chrono::Datelike; + let now = chrono::Utc::now(); + let day_code = match now.weekday() { + chrono::Weekday::Mon => "mon", + chrono::Weekday::Tue => "tue", + chrono::Weekday::Wed => "wed", + chrono::Weekday::Thu => "thu", + chrono::Weekday::Fri => "fri", + chrono::Weekday::Sat => "sat", + chrono::Weekday::Sun => "sun", + }; + let time = now.format("%H:%M").to_string(); + (day_code.to_string(), time) +} + +/// Return the day code for the day *after* the current UTC day (wraps around). +fn tomorrow_day_code() -> String { + use chrono::Datelike; + let tomorrow = chrono::Utc::now() + chrono::Duration::days(1); + match tomorrow.weekday() { + chrono::Weekday::Mon => "mon".to_string(), + chrono::Weekday::Tue => "tue".to_string(), + chrono::Weekday::Wed => "wed".to_string(), + chrono::Weekday::Thu => "thu".to_string(), + chrono::Weekday::Fri => "fri".to_string(), + chrono::Weekday::Sat => "sat".to_string(), + chrono::Weekday::Sun => "sun".to_string(), + } +} + +#[tokio::test] +async fn find_due_schedules_returns_matching_schedule() { + if !require_test_db() { + eprintln!("SKIPPED: TEST_DATABASE_URL not set"); + return; + } + + let app = common::TestApp::new().await; + let (_user_id, session) = app + .create_authenticated_user("find-due-match@example.com") + .await; + + let theme_id = create_theme(&app, &session).await; + + // Use today's day code and "00:00" so the schedule is always already due. + let (day_code, _) = today_day_and_time(); + let body = serde_json::json!({ + "enabled": true, + "days": [day_code], + "time_utc": "00:00", + "emails": [] + }); + let (status, _) = app + .put_with_session( + &format!("/api/v1/themes/{}/schedule", theme_id), + &body, + &session, + ) + .await; + assert_eq!(status, StatusCode::OK); + + let (day_code, current_time) = today_day_and_time(); + let due = db::schedules::find_due_schedules(&app.pool, &day_code, ¤t_time) + .await + .expect("find_due_schedules should not fail"); + + let theme_uuid: uuid::Uuid = theme_id.parse().unwrap(); + assert!( + due.iter().any(|s| s.theme_id == theme_uuid), + "Expected the schedule for theme {} to be returned as due, got: {:?}", + theme_id, + due.iter().map(|s| s.theme_id).collect::>() + ); +} + +#[tokio::test] +async fn find_due_schedules_skips_disabled_schedule() { + if !require_test_db() { + eprintln!("SKIPPED: TEST_DATABASE_URL not set"); + return; + } + + let app = common::TestApp::new().await; + let (_user_id, session) = app + .create_authenticated_user("find-due-disabled@example.com") + .await; + + let theme_id = create_theme(&app, &session).await; + + let (day_code, _) = today_day_and_time(); + let body = serde_json::json!({ + "enabled": false, + "days": [day_code], + "time_utc": "00:00", + "emails": [] + }); + let (status, _) = app + .put_with_session( + &format!("/api/v1/themes/{}/schedule", theme_id), + &body, + &session, + ) + .await; + assert_eq!(status, StatusCode::OK); + + let (day_code, current_time) = today_day_and_time(); + let due = db::schedules::find_due_schedules(&app.pool, &day_code, ¤t_time) + .await + .expect("find_due_schedules should not fail"); + + let theme_uuid: uuid::Uuid = theme_id.parse().unwrap(); + assert!( + !due.iter().any(|s| s.theme_id == theme_uuid), + "Disabled schedule should not appear in due list" + ); +} + +#[tokio::test] +async fn find_due_schedules_skips_already_run_today() { + if !require_test_db() { + eprintln!("SKIPPED: TEST_DATABASE_URL not set"); + return; + } + + let app = common::TestApp::new().await; + let (_user_id, session) = app + .create_authenticated_user("find-due-already-run@example.com") + .await; + + let theme_id = create_theme(&app, &session).await; + + let (day_code, _) = today_day_and_time(); + let body = serde_json::json!({ + "enabled": true, + "days": [day_code], + "time_utc": "00:00", + "emails": [] + }); + let (status, resp) = app + .put_with_session( + &format!("/api/v1/themes/{}/schedule", theme_id), + &body, + &session, + ) + .await; + assert_eq!(status, StatusCode::OK); + + // Mark the schedule as already run + let schedule_id: uuid::Uuid = resp["id"].as_str().unwrap().parse().unwrap(); + db::schedules::mark_run(&app.pool, schedule_id) + .await + .expect("mark_run should not fail"); + + let (day_code, current_time) = today_day_and_time(); + let due = db::schedules::find_due_schedules(&app.pool, &day_code, ¤t_time) + .await + .expect("find_due_schedules should not fail"); + + let theme_uuid: uuid::Uuid = theme_id.parse().unwrap(); + assert!( + !due.iter().any(|s| s.theme_id == theme_uuid), + "Already-run schedule should not appear in due list" + ); +} + +#[tokio::test] +async fn find_due_schedules_skips_wrong_day() { + if !require_test_db() { + eprintln!("SKIPPED: TEST_DATABASE_URL not set"); + return; + } + + let app = common::TestApp::new().await; + let (_user_id, session) = app + .create_authenticated_user("find-due-wrong-day@example.com") + .await; + + let theme_id = create_theme(&app, &session).await; + + // Schedule for tomorrow only, so it must not be due today. + let wrong_day = tomorrow_day_code(); + let body = serde_json::json!({ + "enabled": true, + "days": [wrong_day], + "time_utc": "00:00", + "emails": [] + }); + let (status, _) = app + .put_with_session( + &format!("/api/v1/themes/{}/schedule", theme_id), + &body, + &session, + ) + .await; + assert_eq!(status, StatusCode::OK); + + let (day_code, current_time) = today_day_and_time(); + let due = db::schedules::find_due_schedules(&app.pool, &day_code, ¤t_time) + .await + .expect("find_due_schedules should not fail"); + + let theme_uuid: uuid::Uuid = theme_id.parse().unwrap(); + assert!( + !due.iter().any(|s| s.theme_id == theme_uuid), + "Schedule for a different day should not appear in due list" + ); +}