use http::Method;
use parlov_core::Technique;
use crate::context::ScanContext;
use crate::strategy::Strategy;
use crate::types::{ProbeSpec, RiskLevel, StrategyMetadata};
use crate::util::{build_pair, clone_headers_static};
use super::if_match::TECHNIQUE as IF_MATCH_TECHNIQUE;
const BOGUS_ETAG: &str = "\"zz-if-match-elicit-bogus-etag-000000000000\"";
static METADATA: StrategyMetadata = StrategyMetadata {
strategy_id: "if-match-read-elicit",
strategy_name: "If-Match Read Elicitation",
risk: RiskLevel::Safe,
};
static TECHNIQUE: Technique = IF_MATCH_TECHNIQUE;
pub struct IfMatchReadElicitation;
impl Strategy for IfMatchReadElicitation {
fn metadata(&self) -> &'static StrategyMetadata {
&METADATA
}
fn technique_def(&self) -> &'static Technique {
&TECHNIQUE
}
fn methods(&self) -> &[Method] {
&[Method::GET, Method::HEAD]
}
fn is_applicable(&self, _ctx: &ScanContext) -> bool {
true
}
fn generate(&self, ctx: &ScanContext) -> Vec<ProbeSpec> {
let hdrs = clone_headers_static(&ctx.headers, "if-match", BOGUS_ETAG);
let mut specs = Vec::with_capacity(2);
for method in [Method::GET, Method::HEAD] {
let pair = build_pair(
ctx,
method,
hdrs.clone(),
hdrs.clone(),
None,
METADATA.clone(),
TECHNIQUE,
);
specs.push(ProbeSpec::Pair(pair));
}
specs
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::minimal_ctx;
use crate::types::ProbePair;
use parlov_core::NormativeStrength;
fn find_pair_for<'a>(specs: &'a [ProbeSpec], method: &Method) -> &'a ProbePair {
specs
.iter()
.find_map(|s| {
if let ProbeSpec::Pair(p) = s {
if p.probe.method == *method {
return Some(p);
}
}
None
})
.expect("pair for method must exist")
}
#[test]
fn risk_is_safe() {
assert_eq!(IfMatchReadElicitation.risk(), RiskLevel::Safe);
}
#[test]
fn is_applicable_always_true() {
assert!(IfMatchReadElicitation.is_applicable(&minimal_ctx()));
}
#[test]
fn generate_returns_two_items() {
let specs = IfMatchReadElicitation.generate(&minimal_ctx());
assert_eq!(specs.len(), 2);
for spec in &specs {
assert!(matches!(spec, ProbeSpec::Pair(_)));
}
}
#[test]
fn methods_are_get_and_head() {
let specs = IfMatchReadElicitation.generate(&minimal_ctx());
let methods: Vec<&Method> = specs
.iter()
.filter_map(|s| {
if let ProbeSpec::Pair(p) = s {
Some(&p.probe.method)
} else {
None
}
})
.collect();
assert!(methods.contains(&&Method::GET), "GET must be present");
assert!(methods.contains(&&Method::HEAD), "HEAD must be present");
}
#[test]
fn probe_has_if_match_bogus_etag() {
let specs = IfMatchReadElicitation.generate(&minimal_ctx());
let pair = find_pair_for(&specs, &Method::GET);
assert_eq!(pair.probe.headers.get("if-match").unwrap(), BOGUS_ETAG);
}
#[test]
fn baseline_has_same_if_match_as_probe() {
let specs = IfMatchReadElicitation.generate(&minimal_ctx());
for method in [Method::GET, Method::HEAD] {
let pair = find_pair_for(&specs, &method);
assert_eq!(
pair.baseline.headers.get("if-match").unwrap(),
BOGUS_ETAG,
"baseline must carry If-Match for {method}; single-variable isolation"
);
assert_eq!(
pair.baseline.headers.get("if-match"),
pair.probe.headers.get("if-match"),
"baseline and probe If-Match values must match for {method}"
);
}
}
#[test]
fn baseline_and_probe_headers_are_identical() {
let specs = IfMatchReadElicitation.generate(&minimal_ctx());
for method in [Method::GET, Method::HEAD] {
let pair = find_pair_for(&specs, &method);
assert_eq!(
pair.baseline.headers, pair.probe.headers,
"headers must be byte-identical on both sides for {method}"
);
}
}
#[test]
fn neither_side_has_body() {
let specs = IfMatchReadElicitation.generate(&minimal_ctx());
for method in [Method::GET, Method::HEAD] {
let pair = find_pair_for(&specs, &method);
assert!(
pair.probe.body.is_none(),
"{method} probe must have no body"
);
assert!(
pair.baseline.body.is_none(),
"{method} baseline must have no body"
);
}
}
#[test]
fn technique_strength_is_must() {
let specs = IfMatchReadElicitation.generate(&minimal_ctx());
assert_eq!(specs[0].technique().strength, NormativeStrength::Must);
}
#[test]
fn strategy_id_is_correct() {
assert_eq!(IfMatchReadElicitation.id(), "if-match-read-elicit");
}
}