From 0033269fb879cab7e6227d0fd601f18402effe00 Mon Sep 17 00:00:00 2001 From: batsumaru <> Date: Fri, 16 Oct 2026 15:41:00 +0900 Subject: Add reject future timestamps on write --- src/tests.rs | 144 ++++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 94 insertions(+), 50 deletions(-) (limited to 'src/tests.rs') diff --git a/src/tests.rs b/src/tests.rs index 1f539bd..e97065a 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -6,6 +6,7 @@ use axum::{ header::{CONTENT_TYPE, LOCATION}, }, }; +use chrono::{TimeDelta, Utc}; use http_body_util::BodyExt; use serde_json::{Value, json}; use sqlx::PgPool; @@ -66,10 +67,10 @@ async fn add_waist(app: &Router, measured_at: &str, value: f64) -> String { } async fn seed(app: &Router) { - add_waist(app, "2026-10-14T12:00:00Z", 1.0).await; - add_waist(app, "2026-10-15T00:00:00Z", 2.0).await; - add_waist(app, "2026-10-15T23:59:59.500Z", 3.0).await; - add_waist(app, "2026-10-16T08:00:00Z", 4.0).await; + add_waist(app, "2025-10-14T12:00:00Z", 1.0).await; + add_waist(app, "2025-10-15T00:00:00Z", 2.0).await; + add_waist(app, "2025-10-15T23:59:59.500Z", 3.0).await; + add_waist(app, "2025-10-16T08:00:00Z", 4.0).await; } fn values(rows: &Value) -> Vec { @@ -90,7 +91,7 @@ async fn health_is_ok(pool: PgPool) { #[sqlx::test] async fn create_returns_the_measurement_and_location(pool: PgPool) { let app = router(pool); - let body = measurement("2026-10-14T12:00:00Z", "Waist", 80.5, "Centimeter"); + let body = measurement("2025-10-14T12:00:00Z", "Waist", 80.5, "Centimeter"); let (status, headers, created) = send(&app, Method::POST, "/measurements", Some(body)).await; assert_eq!(status, StatusCode::CREATED); let id = created["id"].as_str().unwrap(); @@ -99,7 +100,7 @@ async fn create_returns_the_measurement_and_location(pool: PgPool) { created, json!({ "id": id, - "measured_at": "2026-10-14T12:00:00Z", + "measured_at": "2025-10-14T12:00:00Z", "metric_type": "Waist", "value": 80.5, "unit": "Centimeter", @@ -121,15 +122,15 @@ async fn create_rejects_bad_bodies(pool: PgPool) { StatusCode::UNPROCESSABLE_ENTITY, ), ( - measurement("2026-10-14T12:00:00Z", "Nope", 1.0, "Kilogram"), + measurement("2025-10-14T12:00:00Z", "Nope", 1.0, "Kilogram"), StatusCode::UNPROCESSABLE_ENTITY, ), ( - measurement("2026-10-14T12:00:00Z", "Waist", -1.0, "Centimeter"), + measurement("2025-10-14T12:00:00Z", "Waist", -1.0, "Centimeter"), StatusCode::UNPROCESSABLE_ENTITY, ), ( - measurement("2026-10-14T12:00:00", "Waist", 80.0, "Centimeter"), + measurement("2025-10-14T12:00:00", "Waist", 80.0, "Centimeter"), StatusCode::UNPROCESSABLE_ENTITY, ), ("not json".to_owned(), StatusCode::BAD_REQUEST), @@ -170,7 +171,7 @@ async fn create_accepts_exactly_the_advertised_pairs(pool: PgPool) { } else { StatusCode::UNPROCESSABLE_ENTITY }; - let body = measurement("2026-10-14T12:00:00Z", metric_type, 70.0, unit); + let body = measurement("2025-10-14T12:00:00Z", metric_type, 70.0, unit); let (status, _, _) = send(&app, Method::POST, "/measurements", Some(body)).await; assert_eq!(status, expected, "{metric_type} {unit}"); } @@ -188,10 +189,10 @@ async fn list_is_empty_array_without_data(pool: PgPool) { #[sqlx::test] async fn list_orders_by_measured_at(pool: PgPool) { let app = router(pool); - add_waist(&app, "2026-10-16T08:00:00Z", 4.0).await; - add_waist(&app, "2026-10-14T12:00:00Z", 1.0).await; - add_waist(&app, "2026-10-15T23:59:59.500Z", 3.0).await; - add_waist(&app, "2026-10-15T00:00:00Z", 2.0).await; + add_waist(&app, "2025-10-16T08:00:00Z", 4.0).await; + add_waist(&app, "2025-10-14T12:00:00Z", 1.0).await; + add_waist(&app, "2025-10-15T23:59:59.500Z", 3.0).await; + add_waist(&app, "2025-10-15T00:00:00Z", 2.0).await; let (_, rows) = get_json(&app, "/measurements").await; assert_eq!(values(&rows), [1.0, 2.0, 3.0, 4.0]); let (_, rows) = get_json(&app, "/measurements?order=asc").await; @@ -214,7 +215,7 @@ async fn list_limit_applies_after_ordering(pool: PgPool) { async fn list_bounds_are_half_open(pool: PgPool) { let app = router(pool); seed(&app).await; - let uri = "/measurements?from=2026-10-15T00:00:00Z&to=2026-10-16T08:00:00Z"; + let uri = "/measurements?from=2025-10-15T00:00:00Z&to=2025-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]); @@ -226,20 +227,20 @@ async fn list_filters_by_utc_timestamps(pool: PgPool) { seed(&app).await; let cases = [ ( - "/measurements?from=2026-10-15T00:00:00Z", + "/measurements?from=2025-10-15T00:00:00Z", vec![2.0, 3.0, 4.0], ), - ("/measurements?to=2026-10-15T00:00:00Z", vec![1.0]), + ("/measurements?to=2025-10-15T00:00:00Z", vec![1.0]), ( - "/measurements?from=2026-10-14T15:00:00Z&to=2026-10-15T15:00:00Z", + "/measurements?from=2025-10-14T15:00:00Z&to=2025-10-15T15:00:00Z", vec![2.0], ), ( - "/measurements?from=2026-10-15T09:00:00%2B09:00", + "/measurements?from=2025-10-15T09:00:00%2B09:00", vec![2.0, 3.0, 4.0], ), ( - "/measurements?from=2026-10-15T00:00:00Z&to=2026-10-15T00:00:00Z", + "/measurements?from=2025-10-15T00:00:00Z&to=2025-10-15T00:00:00Z", vec![], ), ("/measurements?from=2030-01-01T00:00:00Z", vec![]), @@ -256,13 +257,13 @@ async fn list_combines_filters_with_order_and_limit(pool: PgPool) { let app = router(pool); seed(&app).await; let cases = [ - ("/measurements?from=2026-10-15T00:00:00Z&limit=1", vec![2.0]), + ("/measurements?from=2025-10-15T00:00:00Z&limit=1", vec![2.0]), ( - "/measurements?from=2026-10-15T00:00:00Z&order=desc&limit=2", + "/measurements?from=2025-10-15T00:00:00Z&order=desc&limit=2", vec![4.0, 3.0], ), ( - "/measurements?from=2026-10-15T00:00:00Z&to=2026-10-16T00:00:00Z&order=desc", + "/measurements?from=2025-10-15T00:00:00Z&to=2025-10-16T00:00:00Z&order=desc", vec![3.0, 2.0], ), ]; @@ -277,11 +278,11 @@ async fn list_combines_filters_with_order_and_limit(pool: PgPool) { async fn list_filters_by_metric_type(pool: PgPool) { let app = router(pool); let rows = [ - ("2026-10-14T12:00:00Z", "BodyWeight", 70.0, "Kilogram"), - ("2026-10-15T12:00:00Z", "Waist", 80.0, "Centimeter"), - ("2026-10-16T12:00:00Z", "BodyWeight", 71.0, "Kilogram"), - ("2026-10-17T12:00:00Z", "Hip", 95.0, "Centimeter"), - ("2026-10-18T12:00:00Z", "BodyWeight", 72.0, "Kilogram"), + ("2025-10-14T12:00:00Z", "BodyWeight", 70.0, "Kilogram"), + ("2025-10-15T12:00:00Z", "Waist", 80.0, "Centimeter"), + ("2025-10-16T12:00:00Z", "BodyWeight", 71.0, "Kilogram"), + ("2025-10-17T12:00:00Z", "Hip", 95.0, "Centimeter"), + ("2025-10-18T12:00:00Z", "BodyWeight", 72.0, "Kilogram"), ]; for (measured_at, metric_type, value, unit) in rows { let body = measurement(measured_at, metric_type, value, unit); @@ -301,11 +302,11 @@ async fn list_filters_by_metric_type(pool: PgPool) { vec![72.0, 71.0], ), ( - "/measurements?metric_type=BodyWeight&from=2026-10-15T00:00:00Z&to=2026-10-18T00:00:00Z", + "/measurements?metric_type=BodyWeight&from=2025-10-15T00:00:00Z&to=2025-10-18T00:00:00Z", vec![71.0], ), ( - "/measurements?metric_type=Hip&to=2026-10-17T00:00:00Z", + "/measurements?metric_type=Hip&to=2025-10-17T00:00:00Z", vec![], ), ]; @@ -326,11 +327,11 @@ async fn list_rejects_bad_queries(pool: PgPool) { "/measurements?metric_type=Nope", "/measurements?metric_type=bodyweight", "/measurements?from=bad", - "/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", + "/measurements?from=2025-10-15", + "/measurements?to=2025-10-15T00:00:00", + "/measurements?from=2025-10-15T09:00:00+09:00", + "/measurements?date=2025-10-15", + "/measurements?from=2025-10-16T00:00:00Z&to=2025-10-14T00:00:00Z", ]; for uri in uris { let (status, _) = get_json(&app, uri).await; @@ -350,9 +351,9 @@ async fn get_not_found_and_bad_id(pool: PgPool) { #[sqlx::test] async fn update_replaces_the_row(pool: PgPool) { let app = router(pool); - let id = add_waist(&app, "2026-10-14T12:00:00Z", 80.0).await; + let id = add_waist(&app, "2025-10-14T12:00:00Z", 80.0).await; let uri = format!("/measurements/{id}"); - let body = measurement("2026-10-20T09:30:00Z", "Hip", 95.0, "Centimeter"); + let body = measurement("2025-10-20T09:30:00Z", "Hip", 95.0, "Centimeter"); let (status, _, _) = send(&app, Method::PUT, &uri, Some(body)).await; assert_eq!(status, StatusCode::NO_CONTENT); let (_, row) = get_json(&app, &uri).await; @@ -360,7 +361,7 @@ async fn update_replaces_the_row(pool: PgPool) { row, json!({ "id": id, - "measured_at": "2026-10-20T09:30:00Z", + "measured_at": "2025-10-20T09:30:00Z", "metric_type": "Hip", "value": 95.0, "unit": "Centimeter", @@ -371,7 +372,7 @@ async fn update_replaces_the_row(pool: PgPool) { #[sqlx::test] async fn update_missing_id_is_not_found(pool: PgPool) { let app = router(pool); - let body = measurement("2026-10-20T09:30:00Z", "Hip", 95.0, "Centimeter"); + let body = measurement("2025-10-20T09:30:00Z", "Hip", 95.0, "Centimeter"); let uri = format!("/measurements/{}", Uuid::now_v7()); let (status, _, _) = send(&app, Method::PUT, &uri, Some(body)).await; assert_eq!(status, StatusCode::NOT_FOUND); @@ -380,12 +381,12 @@ async fn update_missing_id_is_not_found(pool: PgPool) { #[sqlx::test] async fn update_rejects_invalid_body_and_leaves_row_alone(pool: PgPool) { let app = router(pool); - let id = add_waist(&app, "2026-10-14T12:00:00Z", 80.0).await; + let id = add_waist(&app, "2025-10-14T12:00:00Z", 80.0).await; let uri = format!("/measurements/{id}"); let bodies = [ json!({"x": 1}).to_string(), - measurement("2026-10-20T09:30:00Z", "Waist", 82.0, "Kilogram"), - measurement("2026-10-20T09:30:00Z", "Waist", -1.0, "Centimeter"), + measurement("2025-10-20T09:30:00Z", "Waist", 82.0, "Kilogram"), + measurement("2025-10-20T09:30:00Z", "Waist", -1.0, "Centimeter"), ]; for body in bodies { let (status, _, _) = send(&app, Method::PUT, &uri, Some(body.clone())).await; @@ -398,7 +399,7 @@ async fn update_rejects_invalid_body_and_leaves_row_alone(pool: PgPool) { #[sqlx::test] async fn delete_removes_the_row(pool: PgPool) { let app = router(pool); - let id = add_waist(&app, "2026-10-14T12:00:00Z", 80.0).await; + let id = add_waist(&app, "2025-10-14T12:00:00Z", 80.0).await; let uri = format!("/measurements/{id}"); let (status, _, _) = send(&app, Method::DELETE, &uri, None).await; assert_eq!(status, StatusCode::NO_CONTENT); @@ -409,7 +410,7 @@ async fn delete_removes_the_row(pool: PgPool) { #[sqlx::test] async fn delete_is_idempotent(pool: PgPool) { let app = router(pool); - let id = add_waist(&app, "2026-10-14T12:00:00Z", 80.0).await; + let id = add_waist(&app, "2025-10-14T12:00:00Z", 80.0).await; let uri = format!("/measurements/{id}"); for _ in 0..2 { let (status, _, _) = send(&app, Method::DELETE, &uri, None).await; @@ -426,7 +427,7 @@ async fn old_paths_are_gone(pool: PgPool) { for uri in [ "/", "/all", - "/search?from=2026-10-14", + "/search?from=2025-10-14", "/units", "/metric_types", ] { @@ -439,8 +440,8 @@ async fn old_paths_are_gone(pool: PgPool) { async fn errors_have_a_json_body(pool: PgPool) { let app = router(pool); let missing = format!("/measurements/{}", Uuid::now_v7()); - let valid = measurement("2026-10-14T12:00:00Z", "Waist", 80.0, "Centimeter"); - let invalid_pair = measurement("2026-10-14T12:00:00Z", "Waist", 80.0, "Kilogram"); + let valid = measurement("2025-10-14T12:00:00Z", "Waist", 80.0, "Centimeter"); + let invalid_pair = measurement("2025-10-14T12:00:00Z", "Waist", 80.0, "Kilogram"); let cases = [ ( Method::GET, @@ -501,7 +502,7 @@ async fn error_messages_explain_the_problem(pool: PgPool) { let cases = [ ("/measurements?limit=0", "limit must be positive"), ( - "/measurements?from=2026-10-16T00:00:00Z&to=2026-10-14T00:00:00Z", + "/measurements?from=2025-10-16T00:00:00Z&to=2025-10-14T00:00:00Z", "from must not be after to", ), ]; @@ -509,10 +510,10 @@ 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 (_, body) = get_json(&app, "/measurements?date=2025-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 = measurement("2025-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(); assert!( @@ -520,3 +521,46 @@ async fn error_messages_explain_the_problem(pool: PgPool) { "{message}" ); } + +#[sqlx::test] +async fn create_rejects_future_measured_at_beyond_tolerance(pool: PgPool) { + let app = router(pool); + let cases = [ + (Utc::now() - TimeDelta::days(1), StatusCode::CREATED), + (Utc::now() + TimeDelta::minutes(1), StatusCode::CREATED), + ( + Utc::now() + TimeDelta::hours(1), + StatusCode::UNPROCESSABLE_ENTITY, + ), + ( + Utc::now() + TimeDelta::hours(9), + StatusCode::UNPROCESSABLE_ENTITY, + ), + ]; + for (measured_at, expected) in cases { + let measured_at = measured_at.to_rfc3339(); + let body = measurement(&measured_at, "Waist", 80.0, "Centimeter"); + let (status, _, body) = send(&app, Method::POST, "/measurements", Some(body)).await; + assert_eq!(status, expected, "{measured_at}"); + if expected == StatusCode::UNPROCESSABLE_ENTITY { + let message = body["error"].as_str().unwrap(); + assert!( + message.contains("measured_at is in the future"), + "{message}" + ); + } + } +} + +#[sqlx::test] +async fn update_rejects_future_measured_at(pool: PgPool) { + let app = router(pool); + let id = add_waist(&app, "2025-10-14T12:00:00Z", 80.0).await; + let uri = format!("/measurements/{id}"); + let future = (Utc::now() + TimeDelta::hours(1)).to_rfc3339(); + let body = measurement(&future, "Waist", 82.0, "Centimeter"); + let (status, _, _) = send(&app, Method::PUT, &uri, Some(body)).await; + assert_eq!(status, StatusCode::UNPROCESSABLE_ENTITY); + let (_, row) = get_json(&app, &uri).await; + assert_eq!(row["value"], json!(80.0)); +} -- cgit v1.3