Skip to content

Commit

Permalink
test(image_cache): Add tests for image cache headers
Browse files Browse the repository at this point in the history
  • Loading branch information
CosmicHorrorDev committed Apr 7, 2024
1 parent 5e5a3ab commit 851f7ba
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 25 deletions.
2 changes: 1 addition & 1 deletion src/image/cache/global/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{cmp::Ordering, fs};

use super::{Key, ValidatedImage, Validation, ValidationProbe};
use super::{Key, Validation, ValidationProbe};
use crate::{image::ImageData, utils};

use anyhow::Context;
Expand Down
161 changes: 138 additions & 23 deletions src/image/cache/headers.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
use std::{
fmt,
str::{FromStr, Split},
time::{Duration, SystemTime},
};

use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct CacheControlMeta {
e_tag: Option<ETag>,
stale_after: SystemTime,
}

impl CacheControlMeta {
pub fn from_resp(resp: &ureq::Response) -> Option<Self> {
let e_tag = resp.header("ETag").map(Into::into);
let age = resp.header("Age").and_then(|age| {
Self::from_e_tag_age_and_cache_control_with_time(
resp.header("ETag"),
resp.header("Age"),
resp.header("Cache-Control"),
SystemTime::now(),
)
}

fn from_e_tag_age_and_cache_control_with_time(
e_tag: Option<&str>,
age: Option<&str>,
cache_control: Option<&str>,
time: SystemTime,
) -> Option<Self> {
let e_tag = e_tag.map(Into::into);
let age = age.and_then(|age| {
age.parse::<Age>()
.inspect_err(|err| tracing::info!("Error parsing `Age`: {err:?}"))
.ok()
});
let mut max_age = None;
if let Some(cache_control) = resp.header("Cache-Control") {
if let Some(cache_control) = cache_control {
for directive in CacheControlIter::new(cache_control) {
match directive {
CacheControlDirective::NoStore => return None,
Expand All @@ -35,20 +50,36 @@ impl CacheControlMeta {
if let Some(age) = age {
max_age = max_age.checked_sub(age.0)?;
}
let now = SystemTime::now();
let stale_after = now + max_age;
let stale_after = time + max_age;
Some(Self { stale_after, e_tag })
})
}

fn from_e_tag_life_for_and_time(
e_tag: Option<ETag>,
live_for: Duration,
time: SystemTime,
) -> Self {
let stale_after = time + live_for;
Self { e_tag, stale_after }
}
}

/// Represents the `Age` header
///
/// Can be used in tandem with `Cache-Control` to indicate the current age of the request
struct Age(pub Duration);
struct Age(Duration);

#[derive(Debug)]
struct UnknownAge(pub String);
struct UnknownAge(String);

impl fmt::Display for UnknownAge {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Unknown age: {}", self.0)
}
}

impl std::error::Error for UnknownAge {}

impl FromStr for Age {
type Err = UnknownAge;
Expand All @@ -65,7 +96,7 @@ impl FromStr for Age {
/// tags identify that the content is exactly identical meaning that it can be re-used for things
/// like byte-range requests
// NOTE: We currently ignore weak/strong as we don't need any of "strong"'s guarantees
#[derive(Clone, Debug, Deserialize, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub struct ETag(String);

impl<'a> From<&'a str> for ETag {
Expand Down Expand Up @@ -141,6 +172,17 @@ enum CacheControlParseError {
UnknownAge(UnknownAge),
}

impl fmt::Display for CacheControlParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownDirective(unknown) => write!(f, "Unknown directive: {unknown}"),
Self::UnknownAge(age) => write!(f, "{age}"),
}
}
}

impl std::error::Error for CacheControlParseError {}

impl FromStr for CacheControlDirective {
type Err = CacheControlParseError;

Expand All @@ -158,18 +200,10 @@ impl FromStr for CacheControlDirective {
Ok(Self::MaxAge(age))
}
("no-store", None) => Ok(Self::NoStore),
("stale-while-revalidate", Some(_)) => Ok(Self::Ignored),
(
"s-max-age"
| "no-cache"
| "must-revalidate"
| "proxy-revalidate"
| "private"
| "public"
| "must-understand"
| "no-transform"
| "immutable"
| "stale-while-revalidate"
| "stale-if-error",
"s-max-age" | "no-cache" | "must-revalidate" | "proxy-revalidate" | "private"
| "public" | "must-understand" | "no-transform" | "immutable" | "stale-if-error",
None,
) => Ok(Self::Ignored),
_ => Err(CacheControlParseError::UnknownDirective(s.to_owned())),
Expand All @@ -181,8 +215,89 @@ impl FromStr for CacheControlDirective {
mod tests {
use super::*;

#[test]
fn sanity() {
todo!("Split out parsing logic into its own function and test against that");
macro_rules! c {
($test_name:ident, $headers:expr, expect: None) => {
c!($test_name, $headers, None);
};
($test_name:ident, $headers:expr, expect: (live_for: $live_for:expr)) => {
c!($test_name, $headers, expect: (None, $live_for));
};
($test_name:ident, $headers:expr, expect: ($e_tag:expr, $live_for:expr)) => {
c!($test_name, $headers, Some(CacheControlMeta::from_e_tag_life_for_and_time(
$e_tag,
$live_for,
SystemTime::UNIX_EPOCH,
)));
};
($test_name:ident, $headers:expr, $expect:expr) => {
#[test]
fn $test_name() {
$crate::test_utils::init_test_log();

let Headers { e_tag, age, cache_control } = $headers;
let cache_meta = CacheControlMeta::from_e_tag_age_and_cache_control_with_time(
e_tag,
age,
cache_control,
SystemTime::UNIX_EPOCH,
);
assert_eq!($expect, cache_meta);
}
};
}

fn h() -> Headers {
Headers::default()
}

#[derive(Default)]
struct Headers {
e_tag: Option<&'static str>,
age: Option<&'static str>,
cache_control: Option<&'static str>,
}

impl Headers {
fn e_tag(mut self, e_tag: &'static str) -> Self {
self.e_tag = Some(e_tag);
self
}

fn age(mut self, age: &'static str) -> Self {
self.age = Some(age);
self
}

fn cache(mut self, c_c: &'static str) -> Self {
self.cache_control = Some(c_c);
self
}
}

c!(plain_no_store, h().cache("no-store"), expect: None);
c!(invalid_age, h().age(r#""100""#), expect: None);
c!(invalid_max_age, h().cache(r#"max-age="100""#), expect: None);
c!(age_past_life, h().age("6").cache("max-age=5"), expect: None);
c!(no_must_understand, h().cache("must-understand, no-store"), expect: None);
c!(unknown_directive, h().cache("invalid-directive=100"), expect: None);

static E_TAG: &str = r#"W/"f855e1c49b6a108e1f4f8ac2e759be3275830224c21ac88a5c15bf8a2e6ee30d""#;
static ONE_WEEK: Duration = Duration::from_secs(7 * 24 * 60 * 60);
static HUNDRED_SECS: Duration = Duration::from_secs(100);

c!(sanity, h().cache("max-age=604800"), expect: (live_for: ONE_WEEK));
c!(directive_case_insensitive, h().cache("MaX-aGe=604800"), expect: (live_for: ONE_WEEK));
c!(e_tag, h().cache("max-age=100").e_tag(E_TAG), expect: (Some(E_TAG.into()), HUNDRED_SECS));

c!(
age_and_max_age,
h().cache("max-age=604800").age("100"),
expect: (live_for: ONE_WEEK - HUNDRED_SECS)
);

c!(
several_ignored_directives,
h().cache("max-age=100, must-revalidate, private"),
expect: (live_for: HUNDRED_SECS)
);
}
2 changes: 1 addition & 1 deletion src/image/cache/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ impl ValidatedImage {
/// A probe meant to store some short-lived information for checking if cache entries are valid
// TODO: store system time and
#[derive(Debug)]
struct ValidationProbe(());
pub struct ValidationProbe(());

impl<'a> TryFrom<&Key<'a>> for ValidationProbe {
type Error = anyhow::Error;
Expand Down

0 comments on commit 851f7ba

Please # to comment.