use bytes::Bytes;
use http::{HeaderMap, StatusCode};
use serde::{Deserialize, Serialize};
use crate::{ProbeDefinition, ResponseClass, ResponseSurface, Technique};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProbeExchange {
pub request: ProbeDefinition,
pub response: ResponseSurface,
}
#[derive(Debug, Clone)]
pub struct DifferentialSet {
pub baseline: Vec<ProbeExchange>,
pub probe: Vec<ProbeExchange>,
pub canonical: Option<ProbeExchange>,
pub technique: Technique,
}
impl DifferentialSet {
#[must_use]
pub fn new(
baseline: Vec<ProbeExchange>,
probe: Vec<ProbeExchange>,
technique: Technique,
) -> Self {
Self {
baseline,
probe,
canonical: None,
technique,
}
}
#[must_use]
pub fn first_harvest_exchange(&self) -> Option<(StatusCode, HeaderMap)> {
self.first_harvest_exchange_with_body()
.map(|(s, h, _)| (s, h))
}
#[must_use]
pub fn first_harvest_exchange_with_body(&self) -> Option<(StatusCode, HeaderMap, Bytes)> {
if let Some(ex) = self
.baseline
.iter()
.find(|ex| ex.response.status.is_success())
{
return Some((
ex.response.status,
ex.response.headers.clone(),
ex.response.body.clone(),
));
}
if let Some(ex) = self.baseline.iter().find(|ex| {
ex.response.status.is_redirection()
&& ex.response.headers.contains_key(http::header::LOCATION)
}) {
return Some((
ex.response.status,
ex.response.headers.clone(),
ex.response.body.clone(),
));
}
self.baseline
.iter()
.find(|ex| {
matches!(
ResponseClass::classify(ex.response.status, &ex.response.headers),
ResponseClass::AuthChallenge
| ResponseClass::RateLimited
| ResponseClass::StructuredError
)
})
.map(|ex| {
(
ex.response.status,
ex.response.headers.clone(),
ex.response.body.clone(),
)
})
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use http::{header, HeaderMap, HeaderValue, StatusCode};
use super::*;
use crate::{
always_applicable, NormativeStrength, OracleClass, ProbeDefinition, ResponseSurface,
SignalSurface, Vector,
};
fn technique() -> Technique {
Technique {
id: "test",
name: "Test",
oracle_class: OracleClass::Existence,
vector: Vector::StatusCodeDiff,
strength: NormativeStrength::Must,
normalization_weight: None,
inverted_signal_weight: None,
method_relevant: false,
parser_relevant: false,
applicability: always_applicable,
contradiction_surface: SignalSurface::Status,
}
}
fn make_exchange(status: StatusCode, headers: HeaderMap) -> ProbeExchange {
ProbeExchange {
request: ProbeDefinition {
url: "https://example.com/r/1".into(),
method: http::Method::GET,
headers: HeaderMap::new(),
body: None,
},
response: ResponseSurface {
status,
headers,
body: Bytes::new(),
timing_ns: 0,
},
}
}
fn location_headers() -> HeaderMap {
let mut h = HeaderMap::new();
h.insert(
header::LOCATION,
HeaderValue::from_static("https://example.com/r/2"),
);
h
}
#[test]
fn empty_baseline_returns_none() {
let ds = DifferentialSet {
baseline: vec![],
probe: vec![],
canonical: None,
technique: technique(),
};
assert!(ds.first_harvest_exchange().is_none());
}
#[test]
fn single_200_returns_its_status_and_headers() {
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
let ds = DifferentialSet {
baseline: vec![make_exchange(StatusCode::OK, headers.clone())],
probe: vec![],
canonical: None,
technique: technique(),
};
let (status, h) = ds.first_harvest_exchange().expect("should return Some");
assert_eq!(status, StatusCode::OK);
assert_eq!(
h.get(header::CONTENT_TYPE),
headers.get(header::CONTENT_TYPE)
);
}
#[test]
fn single_301_with_location_returns_it_when_no_2xx() {
let ds = DifferentialSet {
baseline: vec![make_exchange(
StatusCode::MOVED_PERMANENTLY,
location_headers(),
)],
probe: vec![],
canonical: None,
technique: technique(),
};
let (status, h) = ds.first_harvest_exchange().expect("should return Some");
assert_eq!(status, StatusCode::MOVED_PERMANENTLY);
assert!(h.contains_key(header::LOCATION));
}
#[test]
fn single_301_without_location_returns_none() {
let ds = DifferentialSet {
baseline: vec![make_exchange(
StatusCode::MOVED_PERMANENTLY,
HeaderMap::new(),
)],
probe: vec![],
canonical: None,
technique: technique(),
};
assert!(ds.first_harvest_exchange().is_none());
}
#[test]
fn mixed_301_with_location_then_200_returns_200() {
let ds = DifferentialSet {
baseline: vec![
make_exchange(StatusCode::MOVED_PERMANENTLY, location_headers()),
make_exchange(StatusCode::OK, HeaderMap::new()),
],
probe: vec![],
canonical: None,
technique: technique(),
};
let (status, _) = ds.first_harvest_exchange().expect("should return Some");
assert_eq!(status, StatusCode::OK);
}
#[test]
fn mixed_200_then_301_with_location_returns_200() {
let ds = DifferentialSet {
baseline: vec![
make_exchange(StatusCode::OK, HeaderMap::new()),
make_exchange(StatusCode::MOVED_PERMANENTLY, location_headers()),
],
probe: vec![],
canonical: None,
technique: technique(),
};
let (status, _) = ds.first_harvest_exchange().expect("should return Some");
assert_eq!(status, StatusCode::OK);
}
#[test]
fn multiple_2xx_returns_first() {
let mut h1 = HeaderMap::new();
h1.insert(header::ETAG, HeaderValue::from_static("\"abc\""));
let ds = DifferentialSet {
baseline: vec![
make_exchange(StatusCode::OK, h1.clone()),
make_exchange(StatusCode::CREATED, HeaderMap::new()),
],
probe: vec![],
canonical: None,
technique: technique(),
};
let (status, h) = ds.first_harvest_exchange().expect("should return Some");
assert_eq!(status, StatusCode::OK);
assert_eq!(h.get(header::ETAG), h1.get(header::ETAG));
}
}