A device's snooze is clamped to the server's range, 1 minute to 30 days

The server clamped; the core took any i64, so 0 or less set a reminder in the past
and a huge value overflowed Duration::minutes. Every caller passes 60 or 1440
today, so this was latent. Both sides now name the range, SNOOZE_MAX_MINUTES,
and point at each other.

Fixes #5386.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:11:53 -04:00
co-authored by Claude Opus 5.5
parent 7259708f28
commit 1173c1cf12
2 changed files with 31 additions and 1 deletions
+25
View File
@@ -711,8 +711,14 @@ pub fn complete_reminder(conn: &Connection, id: &str) -> rusqlite::Result<Note>
load_note(conn, id)
}
/// The longest snooze, in minutes: 30 days. The server's snooze route clamps to the
/// same range (`notes.SNOOZE_MAX_MINUTES`), so a device and the web agree (#5386).
pub const SNOOZE_MAX_MINUTES: i64 = 60 * 24 * 30;
pub fn snooze_reminder(conn: &Connection, id: &str, minutes: i64) -> rusqlite::Result<Note> {
require_owner(conn, id)?;
// 0 or less would set a reminder in the past; a huge value overflows `Duration`.
let minutes = minutes.clamp(1, SNOOZE_MAX_MINUTES);
let t = (Utc::now() + Duration::minutes(minutes)).to_rfc3339_opts(SecondsFormat::Millis, true);
conn.execute(
"UPDATE notes SET remind_at = ?1 WHERE id = ?2",
@@ -1229,6 +1235,25 @@ mod tests {
assert!(own.shared_by.is_none());
}
#[test]
fn a_snooze_lasts_between_a_minute_and_thirty_days() {
let conn = db();
let id = note(&conn, "call back").id;
// Minutes from just before the call, rounded: the stored time is cut to the
// millisecond, so it can land a hair under the whole minute.
let lands = |minutes: i64| -> i64 {
let before = Utc::now();
let snoozed = snooze_reminder(&conn, &id, minutes).expect("snooze");
let stamp = snoozed.remind_at.expect("set");
let at = DateTime::parse_from_rfc3339(&stamp).expect("rfc3339");
((at.with_timezone(&Utc) - before).num_seconds() + 30) / 60
};
assert_eq!(lands(0), 1, "never in the past");
assert_eq!(lands(-5), 1);
assert_eq!(lands(i64::MAX), SNOOZE_MAX_MINUTES, "no overflow");
assert_eq!(lands(60), 60);
}
#[test]
fn a_view_share_changes_nothing_here() {
let conn = db();