summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/handlers.rs26
-rw-r--r--src/models.rs8
-rw-r--r--src/tests.rs61
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();