style(backend): rustfmt the whole tree; gate cargo fmt --check in CI
The backend had never been run through rustfmt. Doing it in one mechanical pass (134 files) so no future functional diff is buried under formatting churn, then gating `cargo fmt --check` in checks.yml so it stays clean. Formatting only — no logic, SQL, or behaviour changed. Verified after the reformat: cargo test 56 passed, clippy --all-targets -D warnings clean, cargo fmt --check clean. This is the deferred cleanup noted when CI's Format step was first left out. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use sqlx::PgPool;
|
||||
use tokio::sync::{broadcast, Semaphore};
|
||||
use tokio::sync::{Semaphore, broadcast};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::models::upload::Upload;
|
||||
@@ -23,7 +23,12 @@ pub struct CompressionWorker {
|
||||
}
|
||||
|
||||
impl CompressionWorker {
|
||||
pub fn new(pool: PgPool, media_path: PathBuf, concurrency: usize, sse_tx: broadcast::Sender<SseEvent>) -> Self {
|
||||
pub fn new(
|
||||
pool: PgPool,
|
||||
media_path: PathBuf,
|
||||
concurrency: usize,
|
||||
sse_tx: broadcast::Sender<SseEvent>,
|
||||
) -> Self {
|
||||
Self {
|
||||
semaphore: Arc::new(Semaphore::new(concurrency)),
|
||||
pool,
|
||||
@@ -55,7 +60,10 @@ impl CompressionWorker {
|
||||
if worker.generation.load(Ordering::SeqCst) != born_at {
|
||||
return;
|
||||
}
|
||||
match worker.do_process(upload_id, &original_path, &mime_type).await {
|
||||
match worker
|
||||
.do_process(upload_id, &original_path, &mime_type)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
tracing::info!("compression completed for upload {upload_id}");
|
||||
let _ = worker.sse_tx.send(SseEvent {
|
||||
@@ -81,7 +89,8 @@ impl CompressionWorker {
|
||||
}
|
||||
let _ = worker.sse_tx.send(SseEvent {
|
||||
event_type: "upload-error".to_string(),
|
||||
data: serde_json::json!({ "upload_id": upload_id, "error": e.to_string() }).to_string(),
|
||||
data: serde_json::json!({ "upload_id": upload_id, "error": e.to_string() })
|
||||
.to_string(),
|
||||
});
|
||||
let _ = worker.sse_tx.send(SseEvent {
|
||||
event_type: "upload-deleted".to_string(),
|
||||
@@ -103,7 +112,9 @@ impl CompressionWorker {
|
||||
let original = self.media_path.join(original_path);
|
||||
|
||||
if mime_type.starts_with("image/") {
|
||||
let preview_rel = self.generate_image_preview(upload_id, &original, mime_type).await?;
|
||||
let preview_rel = self
|
||||
.generate_image_preview(upload_id, &original, mime_type)
|
||||
.await?;
|
||||
Upload::set_preview_path(&self.pool, upload_id, &preview_rel).await?;
|
||||
tracing::info!("preview generated for upload {upload_id}");
|
||||
} else if mime_type.starts_with("video/") {
|
||||
@@ -151,7 +162,8 @@ impl CompressionWorker {
|
||||
|
||||
// Resize to max 800px wide, preserving aspect ratio
|
||||
let preview = img.resize(800, 800, image::imageops::FilterType::Lanczos3);
|
||||
preview.save_with_format(&preview_path_clone, image::ImageFormat::Jpeg)
|
||||
preview
|
||||
.save_with_format(&preview_path_clone, image::ImageFormat::Jpeg)
|
||||
.context("failed to save preview")?;
|
||||
|
||||
// If the original is PNG, try lossless compression in-place
|
||||
@@ -174,11 +186,7 @@ impl CompressionWorker {
|
||||
Ok(format!("previews/{preview_filename}"))
|
||||
}
|
||||
|
||||
async fn generate_video_thumbnail(
|
||||
&self,
|
||||
upload_id: Uuid,
|
||||
original: &Path,
|
||||
) -> Result<String> {
|
||||
async fn generate_video_thumbnail(&self, upload_id: Uuid, original: &Path) -> Result<String> {
|
||||
let thumbs_dir = self.media_path.join("thumbnails");
|
||||
tokio::fs::create_dir_all(&thumbs_dir).await?;
|
||||
|
||||
@@ -208,18 +216,14 @@ impl CompressionWorker {
|
||||
.spawn()
|
||||
.context("failed to spawn ffmpeg")?;
|
||||
|
||||
let status = match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(120),
|
||||
child.wait(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res.context("ffmpeg wait failed")?,
|
||||
Err(_) => {
|
||||
let _ = child.kill().await;
|
||||
anyhow::bail!("ffmpeg timeout after 120s");
|
||||
}
|
||||
};
|
||||
let status =
|
||||
match tokio::time::timeout(std::time::Duration::from_secs(120), child.wait()).await {
|
||||
Ok(res) => res.context("ffmpeg wait failed")?,
|
||||
Err(_) => {
|
||||
let _ = child.kill().await;
|
||||
anyhow::bail!("ffmpeg timeout after 120s");
|
||||
}
|
||||
};
|
||||
|
||||
if !status.success() {
|
||||
// Best-effort: drain stderr for the log.
|
||||
@@ -228,10 +232,7 @@ impl CompressionWorker {
|
||||
use tokio::io::AsyncReadExt;
|
||||
let _ = handle.read_to_end(&mut stderr).await;
|
||||
}
|
||||
anyhow::bail!(
|
||||
"ffmpeg failed: {}",
|
||||
String::from_utf8_lossy(&stderr)
|
||||
);
|
||||
anyhow::bail!("ffmpeg failed: {}", String::from_utf8_lossy(&stderr));
|
||||
}
|
||||
|
||||
Ok(format!("thumbnails/{thumb_filename}"))
|
||||
|
||||
@@ -81,17 +81,18 @@ impl ConfigCache {
|
||||
}
|
||||
|
||||
// Cache miss or stale — reload the entire table in one query.
|
||||
let rows: Vec<(String, String)> =
|
||||
match sqlx::query_as::<_, (String, String)>("SELECT key, value FROM config")
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = ?e, "config reload failed; using defaults for this read");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let rows: Vec<(String, String)> = match sqlx::query_as::<_, (String, String)>(
|
||||
"SELECT key, value FROM config",
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = ?e, "config reload failed; using defaults for this read");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let values: HashMap<String, String> = rows.into_iter().collect();
|
||||
let result = values.get(key).cloned();
|
||||
@@ -104,26 +105,43 @@ impl ConfigCache {
|
||||
}
|
||||
|
||||
pub async fn get_str(cache: &ConfigCache, key: &str, default: &str) -> String {
|
||||
cache.get_raw(key).await.unwrap_or_else(|| default.to_string())
|
||||
cache
|
||||
.get_raw(key)
|
||||
.await
|
||||
.unwrap_or_else(|| default.to_string())
|
||||
}
|
||||
|
||||
pub async fn get_i64(cache: &ConfigCache, key: &str, default: i64) -> i64 {
|
||||
cache.get_raw(key).await.and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
cache
|
||||
.get_raw(key)
|
||||
.await
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
pub async fn get_usize(cache: &ConfigCache, key: &str, default: usize) -> usize {
|
||||
cache.get_raw(key).await.and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
cache
|
||||
.get_raw(key)
|
||||
.await
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
pub async fn get_f64(cache: &ConfigCache, key: &str, default: f64) -> f64 {
|
||||
cache.get_raw(key).await.and_then(|v| v.parse().ok()).unwrap_or(default)
|
||||
cache
|
||||
.get_raw(key)
|
||||
.await
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
/// Parses common truthy spellings used by both the migration seeds and the admin form.
|
||||
/// Accepts `true/false`, `1/0`, `yes/no`, `on/off` — case-insensitive. Anything else
|
||||
/// returns `default`.
|
||||
pub async fn get_bool(cache: &ConfigCache, key: &str, default: bool) -> bool {
|
||||
let Some(raw) = cache.get_raw(key).await else { return default };
|
||||
let Some(raw) = cache.get_raw(key).await else {
|
||||
return default;
|
||||
};
|
||||
match raw.trim().to_ascii_lowercase().as_str() {
|
||||
"true" | "1" | "yes" | "on" => true,
|
||||
"false" | "0" | "no" | "off" => false,
|
||||
|
||||
@@ -123,20 +123,14 @@ mod tests {
|
||||
#[test]
|
||||
fn picks_longest_matching_mount() {
|
||||
// Both "/" and "/media" prefix the path; the dedicated volume must win.
|
||||
let mounts = vec![
|
||||
("/".to_string(), 100, 40),
|
||||
("/media".to_string(), 200, 150),
|
||||
];
|
||||
let mounts = vec![("/".to_string(), 100, 40), ("/media".to_string(), 200, 150)];
|
||||
let d = select_disk(&mounts, "/media/originals/x.jpg").unwrap();
|
||||
assert_eq!((d.total, d.free), (200, 150));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn falls_back_to_root_when_no_specific_mount_matches() {
|
||||
let mounts = vec![
|
||||
("/".to_string(), 100, 40),
|
||||
("/media".to_string(), 200, 150),
|
||||
];
|
||||
let mounts = vec![("/".to_string(), 100, 40), ("/media".to_string(), 200, 150)];
|
||||
// "/var/lib" is only prefixed by "/".
|
||||
let d = select_disk(&mounts, "/var/lib/data").unwrap();
|
||||
assert_eq!((d.total, d.free), (100, 40));
|
||||
|
||||
@@ -5,12 +5,12 @@ use anyhow::{Context, Result};
|
||||
use async_zip::tokio::write::ZipFileWriter;
|
||||
use async_zip::{Compression, ZipEntryBuilder};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::io::{copy as fcopy, AllowStdIo};
|
||||
use include_dir::{include_dir, Dir};
|
||||
use futures::io::{AllowStdIo, copy as fcopy};
|
||||
use include_dir::{Dir, include_dir};
|
||||
use serde::Serialize;
|
||||
use sqlx::PgPool;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_util::compat::TokioAsyncReadCompatExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -218,7 +218,11 @@ pub async fn invalidate_and_arm(
|
||||
let types: &[&str] = if carried { &["html"] } else { &["zip", "html"] };
|
||||
enqueue_types_at_epoch(&mut *conn, event_id, epoch, types).await?;
|
||||
|
||||
Ok(Some(PendingRegen { event_id, event_name, epoch }))
|
||||
Ok(Some(PendingRegen {
|
||||
event_id,
|
||||
event_name,
|
||||
epoch,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Startup export recovery: re-arm exports for any released event whose keepsake isn't fully
|
||||
@@ -275,7 +279,9 @@ pub async fn recover_exports(
|
||||
continue;
|
||||
}
|
||||
|
||||
tracing::warn!("export recovery: re-arming export jobs for event {event_id} @ epoch {epoch}");
|
||||
tracing::warn!(
|
||||
"export recovery: re-arming export jobs for event {event_id} @ epoch {epoch}"
|
||||
);
|
||||
let mut conn = match pool.acquire().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
@@ -416,7 +422,9 @@ pub fn spawn_export_jobs(
|
||||
if !delay.is_zero() {
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
if let Err(e) = run_zip_export(event_id, epoch, &pool, &media_path, &export_path, &sse_tx).await {
|
||||
if let Err(e) =
|
||||
run_zip_export(event_id, epoch, &pool, &media_path, &export_path, &sse_tx).await
|
||||
{
|
||||
tracing::error!("ZIP export failed for event {event_id} @ epoch {epoch}: {e:#}");
|
||||
mark_failed(&pool, event_id, "zip", epoch, &e.to_string()).await;
|
||||
}
|
||||
@@ -427,9 +435,16 @@ pub fn spawn_export_jobs(
|
||||
if !delay.is_zero() {
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
if let Err(e) =
|
||||
run_html_export(event_id, epoch, &event_name2, &pool2, &media_path2, &export_path2, &sse_tx2)
|
||||
.await
|
||||
if let Err(e) = run_html_export(
|
||||
event_id,
|
||||
epoch,
|
||||
&event_name2,
|
||||
&pool2,
|
||||
&media_path2,
|
||||
&export_path2,
|
||||
&sse_tx2,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::error!("HTML export failed for event {event_id} @ epoch {epoch}: {e:#}");
|
||||
mark_failed(&pool2, event_id, "html", epoch, &e.to_string()).await;
|
||||
@@ -459,8 +474,9 @@ async fn run_zip_export(
|
||||
// here so a failing export can't leak them (which is what fills the disk in the first place).
|
||||
let res = run_zip_export_inner(epoch, event_id, pool, media_path, export_path, sse_tx).await;
|
||||
if res.is_err() {
|
||||
let _ = tokio::fs::remove_file(export_path.join(gen_name(event_id, "Gallery", epoch, ".tmp")))
|
||||
.await;
|
||||
let _ =
|
||||
tokio::fs::remove_file(export_path.join(gen_name(event_id, "Gallery", epoch, ".tmp")))
|
||||
.await;
|
||||
}
|
||||
abandon_if_superseded("ZIP", event_id, epoch, res)
|
||||
}
|
||||
@@ -518,7 +534,11 @@ async fn run_zip_export_inner(
|
||||
let ext = ext_from_path(&row.original_path);
|
||||
let date = row.created_at.format("%Y-%m-%d_%H-%M").to_string();
|
||||
let name_safe = sanitize_name(&row.uploader_name);
|
||||
let folder = if row.mime_type.starts_with("video/") { "Videos" } else { "Photos" };
|
||||
let folder = if row.mime_type.starts_with("video/") {
|
||||
"Videos"
|
||||
} else {
|
||||
"Photos"
|
||||
};
|
||||
let entry_name = format!("{folder}/{date}_{name_safe}_{}.{ext}", row.id);
|
||||
|
||||
let builder = ZipEntryBuilder::new(entry_name.into(), Compression::Stored);
|
||||
@@ -579,7 +599,9 @@ async fn run_zip_export_inner(
|
||||
// IS the publish, atomically. A worker at a dead epoch simply writes a row nobody can see.
|
||||
if !finalize_job(pool, event_id, "zip", epoch, &format!("exports/{out_name}")).await {
|
||||
let _ = tokio::fs::remove_file(&out_path).await;
|
||||
tracing::info!("ZIP export for event {event_id} superseded (epoch {epoch} retired); discarded");
|
||||
tracing::info!(
|
||||
"ZIP export for event {event_id} superseded (epoch {epoch} retired); discarded"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -627,15 +649,25 @@ async fn run_html_export(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let res =
|
||||
run_html_export_inner(epoch, event_id, event_name, pool, media_path, export_path, sse_tx).await;
|
||||
let res = run_html_export_inner(
|
||||
epoch,
|
||||
event_id,
|
||||
event_name,
|
||||
pool,
|
||||
media_path,
|
||||
export_path,
|
||||
sse_tx,
|
||||
)
|
||||
.await;
|
||||
if res.is_err() {
|
||||
// Clean up this generation's temp artifacts so a failing (or abandoned) export can't leak
|
||||
// them — the leak is what fills the disk, which is what corrupts the next archive.
|
||||
let _ =
|
||||
tokio::fs::remove_file(export_path.join(gen_name(event_id, "Memories", epoch, ".tmp"))).await;
|
||||
let _ = tokio::fs::remove_dir_all(export_path.join(format!("viewer_tmp_{event_id}_{epoch}")))
|
||||
.await;
|
||||
tokio::fs::remove_file(export_path.join(gen_name(event_id, "Memories", epoch, ".tmp")))
|
||||
.await;
|
||||
let _ =
|
||||
tokio::fs::remove_dir_all(export_path.join(format!("viewer_tmp_{event_id}_{epoch}")))
|
||||
.await;
|
||||
}
|
||||
abandon_if_superseded("HTML", event_id, epoch, res)
|
||||
}
|
||||
@@ -724,7 +756,10 @@ async fn run_html_export_inner(
|
||||
match ffmpeg_result {
|
||||
Ok(output) if output.status.success() => {}
|
||||
_ => {
|
||||
tracing::warn!("ffmpeg thumbnail failed for upload {}, skipping thumb", row.id);
|
||||
tracing::warn!(
|
||||
"ffmpeg thumbnail failed for upload {}, skipping thumb",
|
||||
row.id
|
||||
);
|
||||
// Missing thumb entry — viewer handles missing thumbs gracefully.
|
||||
}
|
||||
}
|
||||
@@ -904,7 +939,10 @@ async fn run_html_export_inner(
|
||||
let src_file = match tokio::fs::File::open(path).await {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
tracing::warn!("HTML export: skipping media {name} — cannot read {}: {e}", path.display());
|
||||
tracing::warn!(
|
||||
"HTML export: skipping media {name} — cannot read {}: {e}",
|
||||
path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -939,9 +977,19 @@ async fn run_html_export_inner(
|
||||
// Epoch-guarded finalize — writing `done` at a live epoch IS the publish (readiness is derived
|
||||
// from it), so there is no separate ready flag to flip. If our epoch was retired, we lost:
|
||||
// discard the stale archive.
|
||||
if !finalize_job(pool, event_id, "html", epoch, &format!("exports/{out_name}")).await {
|
||||
if !finalize_job(
|
||||
pool,
|
||||
event_id,
|
||||
"html",
|
||||
epoch,
|
||||
&format!("exports/{out_name}"),
|
||||
)
|
||||
.await
|
||||
{
|
||||
let _ = tokio::fs::remove_file(&out_path).await;
|
||||
tracing::info!("HTML export for event {event_id} superseded (epoch {epoch} retired); discarded");
|
||||
tracing::info!(
|
||||
"HTML export for event {event_id} superseded (epoch {epoch} retired); discarded"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -1077,7 +1125,10 @@ async fn finalize_job(
|
||||
/// `Gallery.<n>.zip`, `Gallery.zip.<n>.tmp`, `viewer_tmp_<event>_<n>`). Returns None if the
|
||||
/// name doesn't fit the shape, so unrelated files are left untouched.
|
||||
fn parse_gen_seq(name: &str, prefix: &str, suffix: &str) -> Option<i64> {
|
||||
name.strip_prefix(prefix)?.strip_suffix(suffix)?.parse::<i64>().ok()
|
||||
name.strip_prefix(prefix)?
|
||||
.strip_suffix(suffix)?
|
||||
.parse::<i64>()
|
||||
.ok()
|
||||
}
|
||||
|
||||
/// Best-effort removal of stale per-generation export artifacts for one export type. Deletes
|
||||
@@ -1276,7 +1327,13 @@ fn ext_from_path(path: &str) -> &str {
|
||||
|
||||
fn sanitize_name(name: &str) -> String {
|
||||
name.chars()
|
||||
.map(|c| if c.is_alphanumeric() || c == '-' { c } else { '_' })
|
||||
.map(|c| {
|
||||
if c.is_alphanumeric() || c == '-' {
|
||||
c
|
||||
} else {
|
||||
'_'
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -87,11 +87,7 @@ pub async fn startup_recovery(pool: &PgPool) {
|
||||
/// - drops expired SSE tickets (30s TTL but the map keeps the slot until pruned)
|
||||
///
|
||||
/// Cadence is 1h — fine for both jobs at our scale.
|
||||
pub fn spawn_periodic_tasks(
|
||||
pool: PgPool,
|
||||
rate_limiter: RateLimiter,
|
||||
sse_tickets: SseTicketStore,
|
||||
) {
|
||||
pub fn spawn_periodic_tasks(pool: PgPool, rate_limiter: RateLimiter, sse_tickets: SseTicketStore) {
|
||||
tokio::spawn(async move {
|
||||
let mut tick = tokio::time::interval(Duration::from_secs(3600));
|
||||
// Fire the first tick immediately, then hourly.
|
||||
|
||||
@@ -24,7 +24,12 @@ impl RateLimiter {
|
||||
|
||||
/// Returns `Ok(())` if allowed, `Err(retry_after_secs)` if rate-limited.
|
||||
/// `retry_after_secs` is how long until the oldest slot in the window expires.
|
||||
pub fn check_with_retry(&self, key: impl Into<String>, max: usize, window: Duration) -> Result<(), u64> {
|
||||
pub fn check_with_retry(
|
||||
&self,
|
||||
key: impl Into<String>,
|
||||
max: usize,
|
||||
window: Duration,
|
||||
) -> Result<(), u64> {
|
||||
let now = Instant::now();
|
||||
let key = key.into();
|
||||
let mut map = self.windows.lock().unwrap();
|
||||
@@ -120,7 +125,10 @@ mod tests {
|
||||
assert!(rl.check("k", 1, w));
|
||||
assert!(!rl.check("k", 1, w));
|
||||
std::thread::sleep(Duration::from_millis(55));
|
||||
assert!(rl.check("k", 1, w), "the slot should expire once the window passes");
|
||||
assert!(
|
||||
rl.check("k", 1, w),
|
||||
"the slot should expire once the window passes"
|
||||
);
|
||||
}
|
||||
|
||||
/// `retry_after` is not a "some number in range" — it is the time until the oldest slot
|
||||
@@ -174,7 +182,10 @@ mod tests {
|
||||
let retry = rl.check_with_retry("k", 1, w).unwrap_err();
|
||||
// The sub-second remainder truncates to 0; clients must never be told "retry in 0s"
|
||||
// (that's a busy-loop). The `.max(1)` floor is what prevents it.
|
||||
assert_eq!(retry, 1, "a sub-second remainder must floor to 1, got {retry}");
|
||||
assert_eq!(
|
||||
retry, 1,
|
||||
"a sub-second remainder must floor to 1, got {retry}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -251,7 +262,10 @@ mod tests {
|
||||
fn client_ip_ignores_spoofed_leftmost_entry() {
|
||||
// A client prepending a fake IP to dodge throttles must not win.
|
||||
let mut h = HeaderMap::new();
|
||||
h.insert("x-forwarded-for", "1.2.3.4, 9.9.9.9, 203.0.113.7".parse().unwrap());
|
||||
h.insert(
|
||||
"x-forwarded-for",
|
||||
"1.2.3.4, 9.9.9.9, 203.0.113.7".parse().unwrap(),
|
||||
);
|
||||
assert_eq!(client_ip(&h, "fallback"), "203.0.113.7");
|
||||
}
|
||||
|
||||
|
||||
@@ -90,7 +90,11 @@ mod tests {
|
||||
let ticket = store.issue("hash-1".into());
|
||||
assert_eq!(store.consume(&ticket).as_deref(), Some("hash-1"));
|
||||
// Single-use: a replay of the same ticket is rejected.
|
||||
assert_eq!(store.consume(&ticket), None, "a consumed ticket must not be reusable");
|
||||
assert_eq!(
|
||||
store.consume(&ticket),
|
||||
None,
|
||||
"a consumed ticket must not be reusable"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -131,6 +135,10 @@ mod tests {
|
||||
.expect("host uptime should exceed the ticket TTL"),
|
||||
},
|
||||
);
|
||||
assert_eq!(store.consume(&stale), None, "an expired ticket must not authenticate");
|
||||
assert_eq!(
|
||||
store.consume(&stale),
|
||||
None,
|
||||
"an expired ticket must not authenticate"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user