#![allow(
clippy::disallowed_types,
reason = "dev/verification tooling over JSON artifacts (the catalogue, results, wire \
exchanges), whose shapes belong to the artifacts and the SUT; the carriers \
here are cfg(test)-only, so #[expect] would be unfulfilled in the non-test build"
)]
use std::collections::BTreeMap;
use crate::exec::assertions;
use crate::exec::state::VarStore;
use crate::model::binding::{HeaderExpectation, HeaderMatcher, WireExpectation};
use crate::party::SpecVersions;
#[derive(Debug, Default)]
pub struct RequestContext<'a> {
pub accept: Option<&'a str>,
pub last_version_uid: Option<&'a str>,
pub spec_versions: Option<&'a SpecVersions>,
}
#[must_use]
pub fn evaluate(
expectation: &WireExpectation,
response_headers: &BTreeMap<String, String>,
body: Option<&serde_json::Value>,
ctx: &RequestContext<'_>,
vars: &VarStore,
) -> Vec<String> {
let Some(declared) = expectation.headers.as_deref() else {
return Vec::new();
};
let mut failures = Vec::new();
for (name, declarations) in declared {
for header in declarations.all() {
if !in_scope(header, ctx) {
continue;
}
let observed = header_value(response_headers, name);
if header.optional && observed.is_none_or(|v| v.trim().is_empty()) {
continue;
}
if let Some(failure) = judge(name, &header.matcher, observed, body, ctx, vars) {
failures.push(failure);
}
}
}
failures
}
fn in_scope(header: &HeaderExpectation, ctx: &RequestContext<'_>) -> bool {
match &header.applies {
None => true,
Some(applies) => ctx
.spec_versions
.is_some_and(|versions| applies.satisfied_by(versions)),
}
}
fn header_value<'a>(headers: &'a BTreeMap<String, String>, name: &str) -> Option<&'a str> {
headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(name))
.map(|(_, v)| v.as_str())
}
fn judge(
name: &str,
matcher: &HeaderMatcher,
observed: Option<&str>,
body: Option<&serde_json::Value>,
ctx: &RequestContext<'_>,
vars: &VarStore,
) -> Option<String> {
match matcher {
HeaderMatcher::Present => match observed {
Some(v) if !v.trim().is_empty() => None,
_ => Some(format!("header {name}: expected present, got none")),
},
HeaderMatcher::PresentWithBody => {
crate::exec::bodies::content(body)?;
match observed {
Some(v) if !v.trim().is_empty() => None,
_ => Some(format!(
"header {name}: the response carries a content body, so the header must be present, got none"
)),
}
}
HeaderMatcher::Absent => observed.map(|v| {
format!("header {name}: expected absent, got {v:?} (the binding's cited spec sentence forbids it on this outcome)")
}),
HeaderMatcher::Negotiated => {
let Some(accept) = ctx.accept.map(media_token) else {
return None;
};
match observed {
Some(v) if media_token(v).eq_ignore_ascii_case(accept) => None,
Some(v) => Some(format!(
"header {name}: expected the negotiated media type {accept:?}, got {v:?}"
)),
None => Some(format!(
"header {name}: expected the negotiated media type {accept:?}, got none"
)),
}
}
HeaderMatcher::LatestVersionUid => match observed {
None => Some(format!(
"header {name}: expected the latest version uid, got none"
)),
Some(v) => {
let payload = strip_entity_tag(v);
if payload.is_empty() {
return Some(format!(
"header {name}: expected the latest version uid, got an empty entity tag"
));
}
match ctx.last_version_uid {
Some(latest) if payload.eq_ignore_ascii_case(latest) => None,
Some(latest) => Some(format!(
"header {name}: expected the latest version uid {latest:?}, got {payload:?}"
)),
None => None,
}
}
},
HeaderMatcher::Pattern(pattern) => {
let Some(v) = observed else {
return Some(format!(
"header {name}: expected a value matching {pattern:?}, got none"
));
};
let resolved = match resolve_placeholders(pattern, vars) {
Ok(resolved) => resolved,
Err(missing) => {
return Some(format!(
"header {name}: pattern {pattern:?} names <{missing}>, which is \
neither a captured/with-supplied case variable nor a structural \
token (<n>, <system_id>, <versioned_object_uid>, <template_hrid>) — \
refusing the vacuous wildcard (FerroEHR#1852)"
));
}
};
match regex::Regex::new(&format!("^(?:{resolved})$")) {
Ok(re) if re.is_match(v) => None,
Ok(_) => Some(format!(
"header {name}: value {v:?} does not match {pattern:?} (resolved: {resolved:?})"
)),
Err(e) => Some(format!(
"header {name}: resolved pattern {resolved:?} does not compile: {e}"
)),
}
}
HeaderMatcher::Literal(template) => {
let Some(v) = observed else {
return Some(format!("header {name}: expected a value, got none"));
};
match assertions::render_template(template, vars) {
Ok(want) if want == v => None,
Ok(want) => Some(format!("header {name}: expected {want:?}, got {v:?}")),
Err(e) => Some(format!(
"header {name}: literal template unresolvable ({e}) — declared value cannot be checked"
)),
}
}
}
}
fn media_token(value: &str) -> &str {
value.split(';').next().unwrap_or(value).trim()
}
pub(crate) fn strip_entity_tag(value: &str) -> &str {
let v = value.trim();
let v = v
.strip_prefix("W/")
.or_else(|| v.strip_prefix("w/"))
.unwrap_or(v);
v.trim_matches('"')
}
pub(crate) const VERSION_TREE_ID: &str = r"[1-9][0-9]*(?:\.[0-9]+\.[0-9]+)?";
pub(crate) const UID: &str = concat!(
r"(?:[0-9A-Fa-f]+(?:-[0-9A-Fa-f]+){4}",
r"|[0-9]+(?:\.[0-9]+)*",
r"|(?:[A-Za-z0-9]|[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9])",
r"(?:\.(?:[A-Za-z0-9]|[A-Za-z][A-Za-z0-9_-]*[A-Za-z0-9]))*)",
);
const ARCHETYPE_HRID: &str = concat!(
r"(?:[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)*::)?",
r"[A-Za-z][A-Za-z0-9_]+-[A-Za-z][A-Za-z0-9_]+-[A-Za-z][A-Za-z0-9_]+",
r"\.[A-Za-z][A-Za-z0-9_-]+",
r"\.v[0-9]+\.[0-9]+\.[0-9]+(?:-(?:rc|alpha)(?:\.[0-9]+)?)?",
);
#[must_use]
pub fn structural_token(name: &str) -> Option<&'static str> {
match name {
"n" => Some(VERSION_TREE_ID),
"system_id" | "versioned_object_uid" => Some(UID),
"template_hrid" => Some(ARCHETYPE_HRID),
_ => None,
}
}
fn resolve_placeholders(pattern: &str, vars: &VarStore) -> Result<String, String> {
let mut out = String::new();
let mut rest = pattern;
while let Some(start) = rest.find('<') {
let (head, tail) = rest.split_at(start);
out.push_str(head);
if let Some(end) = tail.find('>') {
let name = tail.get(1..end).unwrap_or_default();
let captured = crate::ids::CaptureName::parse(name)
.ok()
.and_then(|n| vars.scalar(&n).map(str::to_owned));
match (structural_token(name), captured) {
(Some(grammar), _) => out.push_str(grammar),
(None, Some(value)) => out.push_str(®ex::escape(&value)),
(None, None) => return Err(name.to_owned()),
}
rest = tail.get(end + 1..).unwrap_or_default();
} else {
out.push_str(tail);
rest = "";
}
}
out.push_str(rest);
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::exec::state::Captured;
use crate::ids::CaptureName;
fn expectation(headers_yaml: &serde_json::Value) -> WireExpectation {
serde_json::from_value(serde_json::json!({
"status": 200,
"headers": headers_yaml
}))
.unwrap()
}
fn response(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect()
}
fn ctx<'a>() -> RequestContext<'a> {
RequestContext::default()
}
fn versions(its_rest: &str) -> SpecVersions {
SpecVersions {
its_rest: Some(its_rest.to_owned()),
..SpecVersions::default()
}
}
#[test]
fn present_and_absent_are_enforced() {
let e = expectation(&serde_json::json!({ "ETag": "present", "Location": "absent" }));
let ok = response(&[("etag", "W/\"x::sys::1\"")]);
assert!(evaluate(&e, &ok, None, &ctx(), &VarStore::default()).is_empty());
let bad = response(&[("location", "/somewhere")]);
let failures = evaluate(&e, &bad, None, &ctx(), &VarStore::default());
assert_eq!(failures.len(), 2, "{failures:?}");
assert!(failures[0].contains("ETag"));
assert!(failures[1].contains("Location"));
}
#[test]
fn present_optional_never_fails() {
let e = expectation(&serde_json::json!({ "Preference-Applied": "present?" }));
assert!(evaluate(&e, &response(&[]), None, &ctx(), &VarStore::default()).is_empty());
}
#[test]
fn present_with_body_binds_only_a_response_that_carries_content() {
let e = expectation(&serde_json::json!({ "Content-Type": "present-with-body" }));
let detail = serde_json::json!({ "message": "Error message" });
let typed = response(&[("content-type", "application/json")]);
assert!(evaluate(&e, &typed, Some(&detail), &ctx(), &VarStore::default()).is_empty());
let untyped = evaluate(
&e,
&response(&[]),
Some(&detail),
&ctx(),
&VarStore::default(),
);
assert_eq!(untyped.len(), 1, "{untyped:?}");
assert!(untyped[0].contains("carries a content body"), "{untyped:?}");
let blank = response(&[("content-type", " ")]);
assert_eq!(
evaluate(&e, &blank, Some(&detail), &ctx(), &VarStore::default()).len(),
1
);
assert!(evaluate(&e, &response(&[]), None, &ctx(), &VarStore::default()).is_empty());
assert!(evaluate(&e, &typed, None, &ctx(), &VarStore::default()).is_empty());
for empty in [serde_json::Value::Null, serde_json::json!(" ")] {
assert!(
evaluate(
&e,
&response(&[]),
Some(&empty),
&ctx(),
&VarStore::default()
)
.is_empty(),
"{empty} is not a content body"
);
}
}
#[test]
fn the_error_body_media_type_pattern_accepts_the_declared_type_only() {
let e = expectation(&serde_json::json!({
"Content-Type": { "match": "pattern:(?i)application/json(?:\\s*;.*)?", "optional": true }
}));
for value in [
"application/json",
"application/json; charset=utf-8",
"application/json;charset=UTF-8",
"Application/JSON",
] {
let ok = response(&[("content-type", value)]);
assert!(
evaluate(&e, &ok, None, &ctx(), &VarStore::default()).is_empty(),
"{value} is the declared type"
);
}
for value in ["application/xml", "text/plain", "application/jsonx"] {
let bad = response(&[("content-type", value)]);
assert_eq!(
evaluate(&e, &bad, None, &ctx(), &VarStore::default()).len(),
1,
"{value} is not the declared type"
);
}
assert!(evaluate(&e, &response(&[]), None, &ctx(), &VarStore::default()).is_empty());
}
#[test]
fn a_header_sequence_judges_every_declared_expectation() {
let e = expectation(&serde_json::json!({
"ETag": [
"latest-version-uid",
{ "match": "pattern:W/\"[^\"]+\"", "applies": { "its_rest": ">=1.1.0" } }
]
}));
let v11 = versions("1.1.0");
let judged = RequestContext {
last_version_uid: Some("abc::sys::2"),
spec_versions: Some(&v11),
..RequestContext::default()
};
let weak = response(&[("etag", "W/\"abc::sys::2\"")]);
assert!(evaluate(&e, &weak, None, &judged, &VarStore::default()).is_empty());
let bare = response(&[("etag", "\"abc::sys::2\"")]);
let failures = evaluate(&e, &bare, None, &judged, &VarStore::default());
assert_eq!(failures.len(), 1, "{failures:?}");
assert!(failures[0].contains("does not match"), "{failures:?}");
let v103 = versions("1.0.3");
let dated_out = RequestContext {
last_version_uid: Some("abc::sys::2"),
spec_versions: Some(&v103),
..RequestContext::default()
};
assert!(evaluate(&e, &bare, None, &dated_out, &VarStore::default()).is_empty());
let stale = response(&[("etag", "\"abc::sys::1\"")]);
let failures = evaluate(&e, &stale, None, &dated_out, &VarStore::default());
assert_eq!(failures.len(), 1, "{failures:?}");
assert!(failures[0].contains("latest version uid"), "{failures:?}");
}
#[test]
fn negotiated_compares_the_media_token() {
let e = expectation(&serde_json::json!({ "Content-Type": "negotiated" }));
let ctx = RequestContext {
accept: Some("application/json"),
..RequestContext::default()
};
let ok = response(&[("content-type", "application/json; charset=utf-8")]);
assert!(evaluate(&e, &ok, None, &ctx, &VarStore::default()).is_empty());
let bad = response(&[("content-type", "application/openehr.wt+json")]);
assert_eq!(
evaluate(&e, &bad, None, &ctx, &VarStore::default()).len(),
1
);
}
#[test]
fn latest_version_uid_compares_case_insensitively() {
let e = expectation(&serde_json::json!({ "ETag": "latest-version-uid" }));
let ctx = RequestContext {
last_version_uid: Some("abc::sys::2"),
..RequestContext::default()
};
let ok = response(&[("etag", "W/\"ABC::SYS::2\"")]);
assert!(evaluate(&e, &ok, None, &ctx, &VarStore::default()).is_empty());
let stale = response(&[("etag", "W/\"abc::sys::1\"")]);
assert_eq!(
evaluate(&e, &stale, None, &ctx, &VarStore::default()).len(),
1
);
let missing = response(&[]);
assert_eq!(
evaluate(&e, &missing, None, &ctx, &VarStore::default()).len(),
1
);
}
#[test]
fn pattern_resolves_placeholders_from_vars() {
let e = expectation(&serde_json::json!({
"ETag": "pattern:W/\"<contribution_uid>::<system_id>::<n>\""
}));
let mut vars = VarStore::default();
vars.set(
CaptureName::parse("contribution_uid").unwrap(),
Captured::Scalar("abc-123".to_owned()),
);
let ok = response(&[("etag", "W/\"abc-123::any.system::2\"")]);
assert!(evaluate(&e, &ok, None, &ctx(), &vars).is_empty());
let bad = response(&[("etag", "W/\"other-uid::any.system::2\"")]);
assert_eq!(evaluate(&e, &bad, None, &ctx(), &vars).len(), 1);
let empty_system = response(&[("etag", "W/\"abc-123::::2\"")]);
assert_eq!(evaluate(&e, &empty_system, None, &ctx(), &vars).len(), 1);
let zero_led = response(&[("etag", "W/\"abc-123::any.system::02\"")]);
assert_eq!(evaluate(&e, &zero_led, None, &ctx(), &vars).len(), 1);
}
#[test]
fn object_id_token_is_the_released_uid_grammar() {
let e = expectation(&serde_json::json!({
"ETag": "pattern:W/\"<versioned_object_uid>::<system_id>::1\""
}));
for uid in [
"019fcd81-9968-7511-90d8-9f9750ea042a",
"1.2.840.113554.1.2.2",
"uk.nhs.ehr1",
"a",
] {
let etag = format!("W/\"{uid}::ferroehr.local::1\"");
let ok = response(&[("etag", etag.as_str())]);
assert!(
evaluate(&e, &ok, None, &ctx(), &VarStore::default()).is_empty(),
"{uid} is a released uid form"
);
}
for payload in ["::ferroehr.local::1", "-bad::ferroehr.local::1"] {
let etag = format!("W/\"{payload}\"");
let bad = response(&[("etag", etag.as_str())]);
assert_eq!(
evaluate(&e, &bad, None, &ctx(), &VarStore::default()).len(),
1,
"{payload} is not a released uid form"
);
}
}
#[test]
fn system_id_token_is_the_released_uid_grammar() {
let e = expectation(&serde_json::json!({ "ETag": "pattern:W/\"<system_id>\"" }));
for system_id in [
"ferroehr.local",
"openEHRSys.example.com",
"uk.nhs.ehr1",
"1.2.840.113554",
"8849182c-82ad-4088-a07f-48ead4180515",
] {
let etag = format!("W/\"{system_id}\"");
let ok = response(&[("etag", etag.as_str())]);
assert!(
evaluate(&e, &ok, None, &ctx(), &VarStore::default()).is_empty(),
"{system_id} is a released uid form"
);
}
for payload in ["a::b", "sys\\\"tem", "not a uid!", ""] {
let etag = format!("W/\"{payload}\"");
let bad = response(&[("etag", etag.as_str())]);
assert_eq!(
evaluate(&e, &bad, None, &ctx(), &VarStore::default()).len(),
1,
"{payload} is not a released uid form"
);
}
}
#[test]
fn structural_tokens_outrank_a_same_named_variable() {
let e = expectation(&serde_json::json!({
"ETag": "pattern:W/\"<versioned_object_uid>::<system_id>::<n>\""
}));
let mut vars = VarStore::default();
vars.set(
CaptureName::parse("versioned_object_uid").unwrap(),
Captured::Scalar("019fcd6c-d514-7703-9491-b2c8d8413408::ferroehr.local::1".to_owned()),
);
let ok = response(&[(
"etag",
"W/\"019fcd6c-d514-7703-9491-b2c8d8413408::ferroehr.local::1\"",
)]);
assert!(evaluate(&e, &ok, None, &ctx(), &vars).is_empty());
}
#[test]
fn template_hrid_token_is_the_archetype_hrid_grammar() {
let e = expectation(&serde_json::json!({ "ETag": "pattern:W/\"<template_hrid>\"" }));
for hrid in [
"openEHR-EHR-COMPOSITION.cnf_minimal.v1.0.0",
"org.openehr::openEHR-EHR-OBSERVATION.blood_pressure.v2.1.0",
"openEHR-EHR-COMPOSITION.cnf_minimal.v1.0.0-rc.3",
] {
let etag = format!("W/\"{hrid}\"");
let ok = response(&[("etag", etag.as_str())]);
assert!(
evaluate(&e, &ok, None, &ctx(), &VarStore::default()).is_empty(),
"{hrid} is a released HRID"
);
}
let prefix = response(&[("etag", "W/\"openEHR-EHR-COMPOSITION.cnf_minimal.v1\"")]);
assert_eq!(
evaluate(&e, &prefix, None, &ctx(), &VarStore::default()).len(),
1
);
}
#[test]
fn unresolvable_placeholder_is_a_loud_failure_not_a_wildcard() {
let e = expectation(&serde_json::json!({
"ETag": "pattern:W/\"<no_such_capture>::x\""
}));
let would_pass = response(&[("etag", "W/\"anything::x\"")]);
let failures = evaluate(&e, &would_pass, None, &ctx(), &VarStore::default());
assert_eq!(failures.len(), 1, "{failures:?}");
assert!(
failures[0].contains("no_such_capture") && failures[0].contains("FerroEHR#1852"),
"the failure names the unresolvable placeholder: {failures:?}"
);
}
#[test]
fn literal_renders_the_template() {
let e = expectation(&serde_json::json!({ "Preference-Applied": "return=minimal" }));
let ok = response(&[("preference-applied", "return=minimal")]);
assert!(evaluate(&e, &ok, None, &ctx(), &VarStore::default()).is_empty());
let bad = response(&[("preference-applied", "return=representation")]);
assert_eq!(
evaluate(&e, &bad, None, &ctx(), &VarStore::default()).len(),
1
);
}
#[test]
fn header_lookup_is_case_insensitive() {
let e = expectation(&serde_json::json!({ "Last-Modified": "present" }));
let ok = response(&[("LAST-MODIFIED", "Wed, 22 Jul 2009 19:15:56 GMT")]);
assert!(evaluate(&e, &ok, None, &ctx(), &VarStore::default()).is_empty());
}
#[test]
fn optional_presence_still_judges_the_form_when_present() {
let e = expectation(&serde_json::json!({
"ETag": { "match": "pattern:W/\"[^\"]+\"", "optional": true }
}));
assert!(evaluate(&e, &response(&[]), None, &ctx(), &VarStore::default()).is_empty());
let blank = response(&[("etag", " ")]);
assert!(evaluate(&e, &blank, None, &ctx(), &VarStore::default()).is_empty());
let weak = response(&[("etag", "W/\"rs-1\"")]);
assert!(evaluate(&e, &weak, None, &ctx(), &VarStore::default()).is_empty());
let bare = response(&[("etag", "\"rs-1\"")]);
assert_eq!(
evaluate(&e, &bare, None, &ctx(), &VarStore::default()).len(),
1
);
}
#[test]
fn a_dated_expectation_binds_only_the_declaring_releases() {
let e = expectation(&serde_json::json!({
"ETag": {
"match": "pattern:W/\"[^\"]+\"",
"applies": { "its_rest": ">=1.1.0" }
}
}));
let bare = response(&[("etag", "\"x::sys::1\"")]);
let v11 = versions("1.1.0");
let judged = RequestContext {
spec_versions: Some(&v11),
..RequestContext::default()
};
assert_eq!(
evaluate(&e, &bare, None, &judged, &VarStore::default()).len(),
1
);
let v103 = versions("1.0.3");
let dated_out = RequestContext {
spec_versions: Some(&v103),
..RequestContext::default()
};
assert!(evaluate(&e, &bare, None, &dated_out, &VarStore::default()).is_empty());
assert!(evaluate(&e, &bare, None, &ctx(), &VarStore::default()).is_empty());
}
#[test]
fn dating_and_optionality_compose() {
let e = expectation(&serde_json::json!({
"ETag": {
"match": "pattern:W/\"[^\"]+\"",
"optional": true,
"applies": { "its_rest": ">=1.1.0" }
}
}));
let v11 = versions("1.1.0");
let judged = RequestContext {
spec_versions: Some(&v11),
..RequestContext::default()
};
assert!(evaluate(&e, &response(&[]), None, &judged, &VarStore::default()).is_empty());
let bare = response(&[("etag", "\"rs-1\"")]);
assert_eq!(
evaluate(&e, &bare, None, &judged, &VarStore::default()).len(),
1
);
}
#[test]
fn negotiated_without_an_accept_asserts_nothing() {
let e = expectation(&serde_json::json!({ "Content-Type": "negotiated" }));
let any = response(&[("content-type", "application/xml")]);
assert!(evaluate(&e, &any, None, &ctx(), &VarStore::default()).is_empty());
let negotiating = RequestContext {
accept: Some("application/xml"),
..RequestContext::default()
};
let failures = evaluate(&e, &response(&[]), None, &negotiating, &VarStore::default());
assert_eq!(failures.len(), 1, "{failures:?}");
assert!(failures[0].ends_with("got none"), "{failures:?}");
}
#[test]
fn the_latest_version_uid_matcher_degrades_to_presence() {
let e = expectation(&serde_json::json!({ "ETag": "latest-version-uid" }));
let empty = response(&[("etag", "W/\"\"")]);
let with_uid = RequestContext {
last_version_uid: Some("abc::sys::2"),
..RequestContext::default()
};
let failures = evaluate(&e, &empty, None, &with_uid, &VarStore::default());
assert_eq!(failures.len(), 1, "{failures:?}");
assert!(failures[0].contains("empty entity tag"), "{failures:?}");
let anything = response(&[("etag", "\"whatever\"")]);
assert!(evaluate(&e, &anything, None, &ctx(), &VarStore::default()).is_empty());
assert_eq!(
evaluate(&e, &empty, None, &ctx(), &VarStore::default()).len(),
1
);
}
#[test]
fn a_missing_value_is_reported_by_pattern_and_literal_alike() {
let pattern = expectation(&serde_json::json!({ "ETag": "pattern:W/\"[^\"]+\"" }));
let failures = evaluate(&pattern, &response(&[]), None, &ctx(), &VarStore::default());
assert_eq!(failures.len(), 1, "{failures:?}");
assert!(failures[0].contains("got none"), "{failures:?}");
let literal = expectation(&serde_json::json!({ "Location": "/ehr/x" }));
let failures = evaluate(&literal, &response(&[]), None, &ctx(), &VarStore::default());
assert_eq!(failures.len(), 1, "{failures:?}");
assert_eq!(failures[0], "header Location: expected a value, got none");
}
#[test]
fn an_unresolvable_literal_template_is_reported_as_uncheckable() {
let e = expectation(&serde_json::json!({ "Location": "/ehr/${ehr_id}" }));
let observed = response(&[("location", "/ehr/anything")]);
let failures = evaluate(&e, &observed, None, &ctx(), &VarStore::default());
assert_eq!(failures.len(), 1, "{failures:?}");
assert!(
failures[0].contains("literal template unresolvable"),
"{failures:?}"
);
let mut vars = VarStore::default();
vars.set(
CaptureName::parse("ehr_id").unwrap(),
Captured::Scalar("anything".to_owned()),
);
assert!(evaluate(&e, &observed, None, &ctx(), &vars).is_empty());
}
#[test]
fn an_unterminated_placeholder_stays_in_the_pattern() {
let e = expectation(&serde_json::json!({ "ETag": "pattern:W/\"<n\"" }));
let literal_tail = response(&[("etag", "W/\"<n\"")]);
assert!(evaluate(&e, &literal_tail, None, &ctx(), &VarStore::default()).is_empty());
let resolved_as_grammar = response(&[("etag", "W/\"2\"")]);
assert_eq!(
evaluate(&e, &resolved_as_grammar, None, &ctx(), &VarStore::default()).len(),
1,
"an unterminated `<n` never resolves to the version-tree grammar"
);
}
}