use bashkit::Bash;
const WINTER_EPOCH: i64 = 1_705_315_200; const UNAVAILABLE: &str = "named timezones are unavailable";
async fn fixed_date(tz: Option<&str>, script: &str) -> bashkit::ExecResult {
let mut builder = Bash::builder().fixed_epoch(WINTER_EPOCH);
if let Some(tz) = tz {
builder = builder.env("TZ", tz);
}
builder.build().exec(script).await.unwrap()
}
#[tokio::test]
async fn named_zone_errors_instead_of_silently_using_utc() {
let result = fixed_date(Some("America/Chicago"), "date '+%Y-%m-%d %H:%M:%S %Z %z'").await;
assert_eq!(result.exit_code, 1);
assert!(
result.stderr.contains(UNAVAILABLE),
"stderr was {:?}",
result.stderr
);
assert!(result.stdout.is_empty(), "stdout was {:?}", result.stdout);
}
#[tokio::test]
async fn named_zone_error_does_not_echo_the_tz_value() {
for tz in [
"America/Chicago",
"Not/A_Real_Zone",
"CST6CDT,M3.2.0,M11.1.0",
] {
let result = fixed_date(Some(tz), "date '+%Y-%m-%d'").await;
assert!(!result.stdout.contains(tz), "TZ value leaked to stdout");
assert!(!result.stderr.contains(tz), "TZ value leaked to stderr");
}
}
#[tokio::test]
async fn unrecognized_zone_also_errors() {
let result = fixed_date(Some("Not/A_Real_Zone"), "date '+%Y-%m-%d'").await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains(UNAVAILABLE));
}
#[tokio::test]
async fn utc_flag_does_not_excuse_an_unresolvable_tz() {
let result = fixed_date(
Some("America/Chicago"),
"date -u -d '2024-01-15 10:40:00' +%s",
)
.await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains(UNAVAILABLE));
}
#[tokio::test]
async fn unset_empty_and_utc_still_work() {
for tz in [None, Some(""), Some("UTC")] {
let result = fixed_date(tz, "date '+%Y-%m-%d %H:%M:%S %Z %z'").await;
assert_eq!(result.exit_code, 0, "TZ={tz:?}: {}", result.stderr);
assert_eq!(result.stdout.trim(), "2024-01-15 10:40:00 UTC +0000");
}
}
#[tokio::test]
async fn path_style_timezone_still_fails_closed_silently() {
for tz in [":/etc/localtime", "../../etc/localtime"] {
let result = fixed_date(Some(tz), "date '+%Y-%m-%d %H:%M:%S %Z %z'").await;
assert_eq!(result.exit_code, 0, "TZ={tz}: {}", result.stderr);
assert_eq!(
result.stdout.trim(),
"2024-01-15 10:40:00 UTC +0000",
"TZ={tz}"
);
assert!(!result.stderr.contains(tz), "TZ value leaked to stderr");
}
}
#[tokio::test]
async fn explicit_input_offset_still_needs_a_resolvable_tz() {
let result = fixed_date(
Some("America/Chicago"),
"date -d '2024-01-15T12:00:00+02:00' '+%H:%M %Z %z %s'",
)
.await;
assert_eq!(result.exit_code, 1);
assert!(result.stderr.contains(UNAVAILABLE));
let result = fixed_date(
None,
"date -d '2024-01-15T12:00:00+02:00' '+%H:%M %Z %z %s'",
)
.await;
assert_eq!(result.exit_code, 0, "{}", result.stderr);
assert_eq!(result.stdout.trim(), "10:00 UTC +0000 1705312800");
}
#[tokio::test]
async fn error_is_a_clean_diagnostic_not_a_panic_or_debug_dump() {
let result = fixed_date(Some("Europe/Kyiv"), "date").await;
assert_eq!(result.exit_code, 1);
let stderr = String::from_utf8_lossy(result.stderr.as_ref()).to_string();
assert!(stderr.starts_with("date: "), "stderr was {stderr:?}");
assert!(stderr.ends_with('\n'));
assert!(stderr.len() < 200, "diagnostic too long: {stderr:?}");
assert!(!stderr.contains('{'), "looks like a Debug dump: {stderr:?}");
}