use super::{ScdProblemDetailsConsumer, ScdProblemDetailsProducer, TECHNIQUE};
use crate::chain::{Consumer, Producer, ProducerOutput, ProducerOutputKind};
use crate::test_utils::minimal_ctx;
use crate::types::ProbeSpec;
use bytes::Bytes;
use http::{HeaderMap, HeaderValue};
use parlov_core::ResponseClass;
fn content_type(value: &'static str) -> HeaderMap {
let mut h = HeaderMap::new();
h.insert(http::header::CONTENT_TYPE, HeaderValue::from_static(value));
h
}
fn problem_json_body() -> Bytes {
Bytes::from_static(
br#"{"type":"urn:err:missing","errors":[{"field":"name"},{"field":"email"}]}"#,
)
}
fn plain_json_body() -> Bytes {
Bytes::from_static(br#"{"errors":[{"field":"title"}]}"#)
}
#[test]
fn problem_details_producer_admits_client_error() {
assert!(ScdProblemDetailsProducer.admits(ResponseClass::StructuredError));
}
#[test]
fn problem_details_producer_does_not_admit_success() {
assert!(!ScdProblemDetailsProducer.admits(ResponseClass::Success));
}
#[test]
fn problem_details_producer_does_not_admit_not_found() {
assert!(!ScdProblemDetailsProducer.admits(ResponseClass::Other));
}
#[test]
fn problem_details_producer_extracts_from_problem_json_body() {
let headers = content_type("application/problem+json");
let body = problem_json_body();
let out = ScdProblemDetailsProducer.extract_with_body(
ResponseClass::StructuredError,
&headers,
&body,
);
match out {
Some(ProducerOutput::ProblemDetails {
required_fields,
error_type,
}) => {
assert!(
required_fields.contains(&"name".to_owned()),
"must contain field 'name'; got {required_fields:?}"
);
assert!(
required_fields.contains(&"email".to_owned()),
"must contain field 'email'; got {required_fields:?}"
);
assert_eq!(error_type, Some("urn:err:missing".to_owned()));
}
other => panic!("expected ProblemDetails, got {other:?}"),
}
}
#[test]
fn problem_details_producer_extracts_from_plain_json_body() {
let headers = content_type("application/json");
let body = plain_json_body();
let out = ScdProblemDetailsProducer.extract_with_body(
ResponseClass::StructuredError,
&headers,
&body,
);
match out {
Some(ProducerOutput::ProblemDetails {
required_fields, ..
}) => {
assert_eq!(required_fields, vec!["title".to_owned()]);
}
other => panic!("expected ProblemDetails, got {other:?}"),
}
}
#[test]
fn problem_details_producer_returns_none_on_non_json_content_type() {
let headers = content_type("text/html");
let out = ScdProblemDetailsProducer.extract_with_body(
ResponseClass::StructuredError,
&headers,
&problem_json_body(),
);
assert!(out.is_none(), "non-JSON content type must yield None");
}
#[test]
fn problem_details_producer_returns_none_on_empty_body() {
let headers = content_type("application/problem+json");
let out = ScdProblemDetailsProducer.extract_with_body(
ResponseClass::StructuredError,
&headers,
&Bytes::new(),
);
assert!(out.is_none(), "empty body must yield None");
}
#[test]
fn problem_details_producer_returns_none_on_malformed_json() {
let headers = content_type("application/json");
let body = Bytes::from_static(b"{not valid json");
let out = ScdProblemDetailsProducer.extract_with_body(
ResponseClass::StructuredError,
&headers,
&body,
);
assert!(out.is_none(), "malformed JSON must yield None");
}
#[test]
fn problem_details_producer_returns_none_when_no_errors_and_no_type() {
let headers = content_type("application/json");
let body = Bytes::from_static(br#"{"message":"something went wrong"}"#);
let out = ScdProblemDetailsProducer.extract_with_body(
ResponseClass::StructuredError,
&headers,
&body,
);
assert!(
out.is_none(),
"body without errors[] or type must yield None"
);
}
#[test]
fn problem_details_producer_extracts_type_uri_without_errors() {
let headers = content_type("application/problem+json");
let body = Bytes::from_static(br#"{"type":"urn:problem:rate-limited"}"#);
let out = ScdProblemDetailsProducer.extract_with_body(
ResponseClass::StructuredError,
&headers,
&body,
);
match out {
Some(ProducerOutput::ProblemDetails {
required_fields,
error_type,
}) => {
assert!(
required_fields.is_empty(),
"no errors[] means empty required_fields"
);
assert_eq!(error_type, Some("urn:problem:rate-limited".to_owned()));
}
other => panic!("expected ProblemDetails, got {other:?}"),
}
}
#[test]
fn problem_details_consumer_needs_problem_details() {
assert_eq!(
ScdProblemDetailsConsumer.needs(),
ProducerOutputKind::ProblemDetails
);
}
#[test]
fn problem_details_consumer_generates_one_spec_with_required_fields() {
let output = ProducerOutput::ProblemDetails {
required_fields: vec!["quantity".to_owned(), "item_id".to_owned()],
error_type: None,
};
let specs = ScdProblemDetailsConsumer.generate(&minimal_ctx(), &output);
assert_eq!(
specs.len(),
1,
"must generate exactly one spec; got {}",
specs.len()
);
}
#[test]
fn problem_details_consumer_body_contains_all_required_fields() {
let output = ProducerOutput::ProblemDetails {
required_fields: vec!["quantity".to_owned(), "item_id".to_owned()],
error_type: None,
};
let specs = ScdProblemDetailsConsumer.generate(&minimal_ctx(), &output);
let ProbeSpec::Pair(pair) = &specs[0] else {
panic!("expected Pair variant");
};
let body_bytes = pair.baseline.body.as_ref().expect("body must be set");
let body: serde_json::Value =
serde_json::from_slice(body_bytes).expect("body must be valid JSON");
assert!(
body.get("quantity").is_some(),
"body must contain 'quantity'"
);
assert!(body.get("item_id").is_some(), "body must contain 'item_id'");
}
#[test]
fn problem_details_consumer_returns_empty_when_no_required_fields() {
let output = ProducerOutput::ProblemDetails {
required_fields: vec![],
error_type: Some("urn:problem:rate-limited".to_owned()),
};
let specs = ScdProblemDetailsConsumer.generate(&minimal_ctx(), &output);
assert!(
specs.is_empty(),
"empty required_fields must produce no specs"
);
}
#[test]
fn problem_details_consumer_returns_empty_on_wrong_output_variant() {
let output = ProducerOutput::Location("https://example.com".to_owned());
let specs = ScdProblemDetailsConsumer.generate(&minimal_ctx(), &output);
assert!(
specs.is_empty(),
"wrong output variant must produce no specs"
);
}
use proptest::prelude::*;
proptest! {
#[test]
fn producer_any_nonempty_errors_array_yields_problem_details(
fields in prop::collection::vec("[a-z]{1,16}", 1..=8),
) {
let errors_json: String = fields.iter()
.map(|f| format!(r#"{{"field":"{f}"}}"#))
.collect::<Vec<_>>()
.join(",");
let body_str = format!(r#"{{"errors":[{errors_json}]}}"#);
let body = Bytes::from(body_str);
let mut headers = HeaderMap::new();
headers.insert(
http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
);
let out = ScdProblemDetailsProducer.extract_with_body(
ResponseClass::StructuredError,
&headers,
&body,
);
let Some(ProducerOutput::ProblemDetails { required_fields, .. }) = out else {
panic!("expected ProblemDetails output");
};
for f in &fields {
prop_assert!(
required_fields.contains(f),
"field {f} must appear in required_fields; got {required_fields:?}"
);
}
}
#[test]
fn consumer_nonempty_fields_yields_exactly_one_spec(
fields in prop::collection::vec("[a-z]{1,16}", 1..=8),
) {
let output = ProducerOutput::ProblemDetails {
required_fields: fields,
error_type: None,
};
let specs = ScdProblemDetailsConsumer.generate(&minimal_ctx(), &output);
prop_assert_eq!(specs.len(), 1);
}
}
#[test]
fn normalization_weight_is_none() {
assert_eq!(TECHNIQUE.normalization_weight, None);
}
#[test]
fn inverted_signal_weight_is_none() {
assert_eq!(TECHNIQUE.inverted_signal_weight, None);
}