use super::{ScdResourceIdConsumer, ScdResourceIdProducer, TECHNIQUE};
use crate::chain::{Consumer, Producer, ProducerOutput, ProducerOutputKind};
use crate::test_utils::ctx_method_destructive;
use crate::types::ProbeSpec;
use http::{HeaderMap, HeaderValue, Method};
use parlov_core::{ResponseClass, Vector};
#[test]
fn resource_id_producer_admits_success() {
assert!(ScdResourceIdProducer.admits(ResponseClass::Success));
}
#[test]
fn resource_id_producer_does_not_admit_redirect() {
assert!(!ScdResourceIdProducer.admits(ResponseClass::Redirect));
}
#[test]
fn resource_id_producer_does_not_admit_structured_error() {
assert!(!ScdResourceIdProducer.admits(ResponseClass::StructuredError));
}
#[test]
fn resource_id_producer_extracts_id_from_location_header() {
let mut headers = HeaderMap::new();
headers.insert(
http::header::LOCATION,
HeaderValue::from_static("/items/456"),
);
let out = ScdResourceIdProducer.extract(ResponseClass::Success, &headers);
assert_eq!(
out,
Some(ProducerOutput::ResourceId("456".to_owned())),
"must extract last path segment as resource ID"
);
}
#[test]
fn resource_id_producer_extracts_uuid_id() {
let mut headers = HeaderMap::new();
headers.insert(
http::header::LOCATION,
HeaderValue::from_static("/api/v1/resources/550e8400-e29b-41d4-a716-446655440000"),
);
let out = ScdResourceIdProducer.extract(ResponseClass::Success, &headers);
assert_eq!(
out,
Some(ProducerOutput::ResourceId(
"550e8400-e29b-41d4-a716-446655440000".to_owned()
))
);
}
#[test]
fn resource_id_producer_returns_none_when_no_location() {
let out = ScdResourceIdProducer.extract(ResponseClass::Success, &HeaderMap::new());
assert!(out.is_none(), "absent Location must yield None");
}
#[test]
fn resource_id_producer_returns_none_for_root_location() {
let mut headers = HeaderMap::new();
headers.insert(http::header::LOCATION, HeaderValue::from_static("/"));
let out = ScdResourceIdProducer.extract(ResponseClass::Success, &headers);
assert!(out.is_none(), "root-only Location must yield None");
}
#[test]
fn resource_id_producer_returns_none_for_empty_last_segment() {
let mut headers = HeaderMap::new();
headers.insert(http::header::LOCATION, HeaderValue::from_static("/items/"));
let out = ScdResourceIdProducer.extract(ResponseClass::Success, &headers);
assert!(
out.is_none(),
"trailing-slash Location with empty last segment must yield None"
);
}
#[test]
fn resource_id_producer_strips_query_string() {
let mut headers = HeaderMap::new();
headers.insert(
http::header::LOCATION,
HeaderValue::from_static("/items/99?version=1"),
);
let out = ScdResourceIdProducer.extract(ResponseClass::Success, &headers);
assert_eq!(out, Some(ProducerOutput::ResourceId("99".to_owned())));
}
#[test]
fn resource_id_consumer_needs_resource_id() {
assert_eq!(
ScdResourceIdConsumer.needs(),
ProducerOutputKind::ResourceId
);
}
#[test]
fn resource_id_consumer_generates_three_specs() {
let output = ProducerOutput::ResourceId("42".to_owned());
let specs = ScdResourceIdConsumer.generate(&ctx_method_destructive(), &output);
assert_eq!(
specs.len(),
3,
"must generate GET, PATCH, DELETE; got {}",
specs.len()
);
}
#[test]
fn resource_id_consumer_get_spec_targets_resource_url() {
let output = ProducerOutput::ResourceId("42".to_owned());
let ctx = ctx_method_destructive();
let specs = ScdResourceIdConsumer.generate(&ctx, &output);
let get_pair = specs
.iter()
.find_map(|s| {
if let ProbeSpec::Pair(p) = s {
if p.baseline.method == Method::GET {
return Some(p);
}
}
None
})
.expect("GET spec must exist");
assert!(
get_pair.baseline.url.contains("42"),
"GET baseline URL must embed extracted ID '42'; got {}",
get_pair.baseline.url
);
assert!(
get_pair.probe.url.contains(&ctx.probe_id),
"GET probe URL must embed probe_id; got {}",
get_pair.probe.url
);
}
#[test]
fn resource_id_consumer_delete_spec_targets_resource_url() {
let output = ProducerOutput::ResourceId("42".to_owned());
let ctx = ctx_method_destructive();
let specs = ScdResourceIdConsumer.generate(&ctx, &output);
let del_pair = specs
.iter()
.find_map(|s| {
if let ProbeSpec::Pair(p) = s {
if p.baseline.method == Method::DELETE {
return Some(p);
}
}
None
})
.expect("DELETE spec must exist");
assert!(
del_pair.baseline.url.contains("42"),
"DELETE baseline URL must embed extracted ID; got {}",
del_pair.baseline.url
);
assert!(
del_pair.baseline.body.is_none(),
"DELETE baseline must have no body"
);
assert!(
del_pair.probe.body.is_none(),
"DELETE probe must have no body"
);
}
#[test]
fn resource_id_consumer_patch_has_body() {
let output = ProducerOutput::ResourceId("42".to_owned());
let specs = ScdResourceIdConsumer.generate(&ctx_method_destructive(), &output);
let patch_pair = specs
.iter()
.find_map(|s| {
if let ProbeSpec::Pair(p) = s {
if p.baseline.method == Method::PATCH {
return Some(p);
}
}
None
})
.expect("PATCH spec must exist");
assert!(
patch_pair.baseline.body.is_some(),
"PATCH baseline must have a body"
);
assert!(
patch_pair.probe.body.is_some(),
"PATCH probe must have a body"
);
}
#[test]
fn resource_id_consumer_returns_empty_when_wrong_output_variant() {
let output = ProducerOutput::Location("https://example.com/items/42".to_owned());
let specs = ScdResourceIdConsumer.generate(&ctx_method_destructive(), &output);
assert!(
specs.is_empty(),
"non-ResourceId output must produce no specs"
);
}
#[test]
fn resource_id_consumer_technique_is_status_code_diff() {
let output = ProducerOutput::ResourceId("1".to_owned());
let specs = ScdResourceIdConsumer.generate(&ctx_method_destructive(), &output);
assert_eq!(specs[0].technique().vector, Vector::StatusCodeDiff);
}
use proptest::prelude::*;
proptest! {
#[test]
fn producer_any_non_empty_last_segment_yields_resource_id(
prefix in "[a-z]{1,8}(/[a-z0-9]{1,8}){0,3}",
id in "[a-z0-9]{1,36}",
) {
let location = format!("/{prefix}/{id}");
let Ok(val) = HeaderValue::from_str(&location) else { return Ok(()); };
let mut headers = HeaderMap::new();
headers.insert(http::header::LOCATION, val);
let out = ScdResourceIdProducer.extract(ResponseClass::Success, &headers);
prop_assert_eq!(out, Some(ProducerOutput::ResourceId(id)));
}
}
proptest! {
#[test]
fn consumer_any_resource_id_yields_three_specs(
id in "[a-z0-9]{1,36}",
) {
let output = ProducerOutput::ResourceId(id);
let specs = ScdResourceIdConsumer.generate(&ctx_method_destructive(), &output);
prop_assert_eq!(specs.len(), 3);
}
}
proptest! {
#[test]
fn consumer_all_specs_are_pairs(id in "[a-z0-9]{1,36}") {
let output = ProducerOutput::ResourceId(id);
let specs = ScdResourceIdConsumer.generate(&ctx_method_destructive(), &output);
for spec in &specs {
prop_assert!(matches!(spec, ProbeSpec::Pair(_)), "all specs must be Pair");
}
}
}
#[test]
fn normalization_weight_is_0_12() {
assert_eq!(TECHNIQUE.normalization_weight, Some(0.12));
}
#[test]
fn inverted_signal_weight_is_none() {
assert_eq!(TECHNIQUE.inverted_signal_weight, None);
}