use autumn_web::prelude::*;
use autumn_web::test::TestApp;
use autumn_web::time::{Clock, FixedClock, TickingClock};
use chrono::{TimeZone, Utc};
use std::time::Duration;
#[get("/now")]
async fn current_time(clock: Clock) -> String {
clock.now().to_rfc3339()
}
#[get("/token-check")]
async fn token_check(clock: Clock) -> axum::http::StatusCode {
let issued_at = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let expires_at = issued_at + chrono::Duration::days(30);
if clock.now() < expires_at {
axum::http::StatusCode::OK
} else {
axum::http::StatusCode::UNAUTHORIZED
}
}
#[tokio::test]
async fn clock_extractor_works_with_default_real_clock() {
let client = TestApp::new().routes(routes![current_time]).build();
let resp = client.get("/now").send().await;
resp.assert_ok();
let body = resp.text();
chrono::DateTime::parse_from_rfc3339(&body).expect("should be valid RFC 3339 from SystemClock");
}
#[tokio::test]
async fn fixed_clock_pins_time() {
let pinned = Utc.with_ymd_and_hms(2025, 6, 15, 12, 0, 0).unwrap();
let client = TestApp::new()
.routes(routes![current_time])
.with_clock(FixedClock::at(pinned))
.build();
let body = client.get("/now").send().await.assert_ok().text();
let got = chrono::DateTime::parse_from_rfc3339(&body).expect("valid RFC 3339");
assert_eq!(got.with_timezone(&Utc), pinned);
}
#[tokio::test]
async fn ticking_clock_advances_between_requests() {
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let clock = TickingClock::starting_at(start);
let client = TestApp::new()
.routes(routes![current_time])
.with_clock(clock.clone())
.build();
let body = client.get("/now").send().await.assert_ok().text();
let t1 = chrono::DateTime::parse_from_rfc3339(&body)
.expect("valid RFC 3339")
.with_timezone(&Utc);
assert_eq!(t1, start);
client.advance_clock(Duration::from_secs(3600));
let body = client.get("/now").send().await.assert_ok().text();
let t2 = chrono::DateTime::parse_from_rfc3339(&body)
.expect("valid RFC 3339")
.with_timezone(&Utc);
assert_eq!(t2, start + chrono::Duration::hours(1));
}
#[tokio::test]
async fn token_expires_after_30_days_no_sleep() {
let issued_at = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let client = TestApp::new()
.routes(routes![token_check])
.with_clock(TickingClock::starting_at(issued_at))
.build();
client.get("/token-check").send().await.assert_status(200);
client.advance_clock(Duration::from_secs(29 * 24 * 3600));
client.get("/token-check").send().await.assert_status(200);
client.advance_clock(Duration::from_secs(2 * 24 * 3600));
client.get("/token-check").send().await.assert_status(401);
}
#[tokio::test]
async fn app_without_custom_clock_uses_real_wall_clock() {
let client = TestApp::new().routes(routes![current_time]).build();
let resp = client.get("/now").send().await;
resp.assert_ok();
let body = resp.text();
let got = chrono::DateTime::parse_from_rfc3339(&body).expect("valid RFC 3339");
let diff = (Utc::now() - got.with_timezone(&Utc)).num_seconds().abs();
assert!(diff < 5, "system clock diff should be < 5s, got {diff}s");
}
#[tokio::test]
async fn advance_clock_with_fixed_clock_is_noop() {
let pinned = Utc.with_ymd_and_hms(2025, 3, 1, 0, 0, 0).unwrap();
let client = TestApp::new()
.routes(routes![current_time])
.with_clock(FixedClock::at(pinned))
.build();
client.advance_clock(Duration::from_secs(86400));
let body = client.get("/now").send().await.assert_ok().text();
let got = chrono::DateTime::parse_from_rfc3339(&body)
.expect("valid RFC 3339")
.with_timezone(&Utc);
assert_eq!(got, pinned);
}
#[tokio::test]
async fn scheduler_tick_key_is_deterministic_with_clock() {
use autumn_web::time::{FixedClock, clock_unix_secs};
let pinned = Utc.with_ymd_and_hms(2024, 3, 15, 10, 30, 0).unwrap();
let clock = FixedClock::at(pinned);
let secs = clock_unix_secs(&clock);
let tick_key = autumn_web::scheduler::fixed_delay_tick_key(
"my_task",
Duration::from_secs(60),
Duration::from_secs(secs),
);
let tick_key2 = autumn_web::scheduler::fixed_delay_tick_key(
"my_task",
Duration::from_secs(60),
Duration::from_secs(clock_unix_secs(&clock)),
);
assert_eq!(tick_key, tick_key2, "same clock time → same tick key");
let tick_key_before_flip = autumn_web::scheduler::fixed_delay_tick_key(
"my_task",
Duration::from_secs(60),
Duration::from_secs(secs + 59),
);
assert_eq!(
tick_key, tick_key_before_flip,
"within interval → same bucket"
);
let tick_key_after_flip = autumn_web::scheduler::fixed_delay_tick_key(
"my_task",
Duration::from_secs(60),
Duration::from_secs(secs + 60),
);
assert_ne!(
tick_key, tick_key_after_flip,
"one interval later → new bucket"
);
}
#[cfg(feature = "storage")]
#[tokio::test]
async fn signed_url_expiry_is_deterministic_with_clock() {
use autumn_web::storage::local::{SigningKey, sign, verify_with_now};
use autumn_web::time::{TickingClock, clock_unix_secs};
let key = SigningKey::new(b"test-signing-key-32-bytes-long!!".to_vec());
let blob_key = "avatars/me.png";
let start = Utc.with_ymd_and_hms(2025, 1, 1, 0, 0, 0).unwrap();
let clock = TickingClock::starting_at(start);
let now_unix = clock_unix_secs(&clock);
let expires_at = now_unix + 300;
let signature = sign(key.as_bytes(), blob_key, expires_at);
assert!(
verify_with_now(key.as_bytes(), blob_key, expires_at, &signature, now_unix).is_ok(),
"URL should be valid at issue time"
);
clock.advance(Duration::from_secs(299));
let now_unix = clock_unix_secs(&clock);
assert!(
verify_with_now(key.as_bytes(), blob_key, expires_at, &signature, now_unix).is_ok(),
"URL should be valid 1s before expiry"
);
clock.advance(Duration::from_secs(2));
let now_unix = clock_unix_secs(&clock);
assert!(
verify_with_now(key.as_bytes(), blob_key, expires_at, &signature, now_unix).is_err(),
"URL should be expired after TTL"
);
}