diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/handlers.rs | 26 | ||||
| -rw-r--r-- | src/models.rs | 8 | ||||
| -rw-r--r-- | src/tests.rs | 61 |
3 files changed, 45 insertions, 50 deletions
diff --git a/src/handlers.rs b/src/handlers.rs index 50ae9e2..3c38724 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -3,7 +3,6 @@ use axum::{ http::{HeaderName, StatusCode, header}, response::Json, }; -use chrono::NaiveTime; use sqlx::PgPool; use strum::IntoEnumIterator; use uuid::Uuid; @@ -27,30 +26,11 @@ pub async fn list_measurements( None => None, _ => return Err(AppError::BadRequest("limit must be positive")), }; - let (from, to) = match (query.date, query.from, query.to) { - (Some(date), None, None) => (Some(date), Some(date)), - (None, from, to) => (from, to), - _ => { - return Err(AppError::BadRequest( - "date can't be combined with from or to", - )); - } - }; - if let (Some(from), Some(to)) = (from, to) + if let (Some(from), Some(to)) = (query.from, query.to) && from > to { return Err(AppError::BadRequest("from must not be after to")); } - let start = from.map(|from| from.and_time(NaiveTime::MIN).and_utc()); - let end = match to { - Some(to) => { - let Some(next_day) = to.succ_opt() else { - return Err(AppError::BadRequest("to is out of range")); - }; - Some(next_day.and_time(NaiveTime::MIN).and_utc()) - } - None => None, - }; // SQL can't bind ASC/DESC as a parameter. The CASE yields measured_at only when // `order` is desc (sorted DESC) and NULL otherwise, so that key ties for every // row and the ASC key decides the order. If this gets slow, use an if/else with a @@ -65,8 +45,8 @@ pub async fn list_measurements( measured_at ASC LIMIT $3 ", - start, - end, + query.from, + query.to, limit, matches!(query.order, Order::Desc) ) diff --git a/src/models.rs b/src/models.rs index 7096c64..102860b 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, NaiveDate, Utc}; +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use strum::{EnumIter, IntoStaticStr}; use uuid::Uuid; @@ -91,10 +91,10 @@ pub enum Order { } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] pub struct ListQuery { - pub date: Option<NaiveDate>, - pub from: Option<NaiveDate>, - pub to: Option<NaiveDate>, + pub from: Option<DateTime<Utc>>, + pub to: Option<DateTime<Utc>>, pub limit: Option<i64>, #[serde(default)] pub order: Order, diff --git a/src/tests.rs b/src/tests.rs index 5b21352..d2ee143 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -128,6 +128,10 @@ async fn create_rejects_bad_bodies(pool: PgPool) { measurement("2026-10-14T12:00:00Z", "Waist", -1.0, "Centimeter"), StatusCode::UNPROCESSABLE_ENTITY, ), + ( + measurement("2026-10-14T12:00:00", "Waist", 80.0, "Centimeter"), + StatusCode::UNPROCESSABLE_ENTITY, + ), ("not json".to_owned(), StatusCode::BAD_REQUEST), ]; for (body, expected) in cases { @@ -207,30 +211,38 @@ async fn list_limit_applies_after_ordering(pool: PgPool) { } #[sqlx::test] -async fn list_single_date_includes_the_whole_day(pool: PgPool) { +async fn list_bounds_are_half_open(pool: PgPool) { let app = router(pool); seed(&app).await; - let (status, rows) = get_json(&app, "/measurements?date=2026-10-15").await; + let uri = "/measurements?from=2026-10-15T00:00:00Z&to=2026-10-16T08:00:00Z"; + let (status, rows) = get_json(&app, uri).await; assert_eq!(status, StatusCode::OK); assert_eq!(values(&rows), [2.0, 3.0]); } #[sqlx::test] -async fn list_ranges_are_inclusive_of_both_days(pool: PgPool) { +async fn list_filters_by_utc_timestamps(pool: PgPool) { let app = router(pool); seed(&app).await; let cases = [ - ("/measurements?from=2026-10-15", vec![2.0, 3.0, 4.0]), - ("/measurements?to=2026-10-14", vec![1.0]), ( - "/measurements?from=2026-10-14&to=2026-10-15", - vec![1.0, 2.0, 3.0], + "/measurements?from=2026-10-15T00:00:00Z", + vec![2.0, 3.0, 4.0], + ), + ("/measurements?to=2026-10-15T00:00:00Z", vec![1.0]), + ( + "/measurements?from=2026-10-14T15:00:00Z&to=2026-10-15T15:00:00Z", + vec![2.0], + ), + ( + "/measurements?from=2026-10-15T09:00:00%2B09:00", + vec![2.0, 3.0, 4.0], ), ( - "/measurements?from=2026-10-15&to=2026-10-15", - vec![2.0, 3.0], + "/measurements?from=2026-10-15T00:00:00Z&to=2026-10-15T00:00:00Z", + vec![], ), - ("/measurements?from=2030-01-01", vec![]), + ("/measurements?from=2030-01-01T00:00:00Z", vec![]), ]; for (uri, expected) in cases { let (status, rows) = get_json(&app, uri).await; @@ -244,12 +256,15 @@ async fn list_combines_filters_with_order_and_limit(pool: PgPool) { let app = router(pool); seed(&app).await; let cases = [ - ("/measurements?from=2026-10-15&limit=1", vec![2.0]), + ("/measurements?from=2026-10-15T00:00:00Z&limit=1", vec![2.0]), ( - "/measurements?from=2026-10-15&order=desc&limit=2", + "/measurements?from=2026-10-15T00:00:00Z&order=desc&limit=2", vec![4.0, 3.0], ), - ("/measurements?date=2026-10-15&order=desc", vec![3.0, 2.0]), + ( + "/measurements?from=2026-10-15T00:00:00Z&to=2026-10-16T00:00:00Z&order=desc", + vec![3.0, 2.0], + ), ]; for (uri, expected) in cases { let (status, rows) = get_json(&app, uri).await; @@ -265,11 +280,12 @@ async fn list_rejects_bad_queries(pool: PgPool) { "/measurements?limit=0", "/measurements?limit=-1", "/measurements?order=sideways", - "/measurements?date=bad", "/measurements?from=bad", - "/measurements?date=2026-10-15&from=2026-10-14", - "/measurements?date=2026-10-15&to=2026-10-16", - "/measurements?from=2026-10-16&to=2026-10-14", + "/measurements?from=2026-10-15", + "/measurements?to=2026-10-15T00:00:00", + "/measurements?from=2026-10-15T09:00:00+09:00", + "/measurements?date=2026-10-15", + "/measurements?from=2026-10-16T00:00:00Z&to=2026-10-14T00:00:00Z", ]; for uri in uris { let (status, _) = get_json(&app, uri).await; @@ -395,7 +411,7 @@ async fn errors_have_a_json_body(pool: PgPool) { ), ( Method::GET, - "/measurements?date=bad".to_owned(), + "/measurements?from=bad".to_owned(), None, StatusCode::BAD_REQUEST, ), @@ -440,11 +456,7 @@ async fn error_messages_explain_the_problem(pool: PgPool) { let cases = [ ("/measurements?limit=0", "limit must be positive"), ( - "/measurements?date=2026-10-15&from=2026-10-14", - "date can't be combined with from or to", - ), - ( - "/measurements?from=2026-10-16&to=2026-10-14", + "/measurements?from=2026-10-16T00:00:00Z&to=2026-10-14T00:00:00Z", "from must not be after to", ), ]; @@ -452,6 +464,9 @@ async fn error_messages_explain_the_problem(pool: PgPool) { let (_, body) = get_json(&app, uri).await; assert_eq!(body["error"], message, "{uri}"); } + let (_, body) = get_json(&app, "/measurements?date=2026-10-15").await; + let message = body["error"].as_str().unwrap(); + assert!(message.contains("unknown field `date`"), "{message}"); let body = measurement("2026-10-14T12:00:00Z", "Waist", 80.0, "Kilogram"); let (_, _, body) = send(&app, Method::POST, "/measurements", Some(body)).await; let message = body["error"].as_str().unwrap(); |
