use crate::core::config::e2e::{ArgMapping, CallConfig};
use crate::e2e::fixture::Fixture;
use crate::e2e::validate::{Severity, ValidationError};
pub(super) fn check_preserve_input_urls(
fixture: &Fixture,
call_config: &CallConfig,
errors: &mut Vec<ValidationError>,
) {
let url_args: Vec<&ArgMapping> = fixture
.resolved_args(call_config)
.iter()
.filter(|arg| matches!(arg.arg_type.as_str(), "mock_url" | "mock_url_list"))
.collect();
if fixture.preserve_input_urls {
if url_args.is_empty() {
errors.push(ValidationError {
file: fixture.source.clone(),
message: format!(
"fixture '{}' sets preserve_input_urls but call '{}' has no mock_url or \
mock_url_list argument, so the flag has no effect",
fixture.id,
fixture.call.as_deref().unwrap_or("<default>")
),
severity: Severity::Error,
});
}
return;
}
for arg in url_args {
let Some(literal) = discarded_literal(&fixture.input, arg) else {
continue;
};
errors.push(ValidationError {
file: fixture.source.clone(),
message: format!(
"fixture '{}' declares the absolute URL '{}' for {} argument '{}' but does not set \
preserve_input_urls, so the literal is discarded and the mock server address is \
bound instead -- set preserve_input_urls = true if the URL is meaningful to the \
test, or declare a mock-server-relative path instead",
fixture.id, literal, arg.arg_type, arg.name
),
severity: Severity::Error,
});
}
}
fn discarded_literal<'a>(input: &'a serde_json::Value, arg: &ArgMapping) -> Option<&'a str> {
if arg.arg_type == "mock_url_list" {
return crate::e2e::codegen::resolve_urls_field(input, &arg.field)
.as_array()?
.iter()
.filter_map(serde_json::Value::as_str)
.find(|element| has_url_scheme(element));
}
crate::e2e::codegen::resolve_field(input, &arg.field)
.as_str()
.filter(|value| has_url_scheme(value))
}
fn has_url_scheme(value: &str) -> bool {
value.contains("://")
}
#[cfg(test)]
mod tests {
use crate::core::config::e2e::CallConfig;
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::Fixture;
use crate::e2e::validate::{Severity, validate_fixtures_semantic};
fn make_e2e_config() -> E2eConfig {
E2eConfig::default()
}
fn url_fixture(arg_type: &str, field: &str, input: serde_json::Value) -> Fixture {
Fixture {
id: "literal_url".to_string(),
source: "literal_url.json".to_string(),
input,
args: vec![
serde_json::from_value(serde_json::json!({
"name": "url",
"field": format!("input.{field}"),
"type": arg_type,
}))
.expect("argument mapping should deserialize"),
],
..Fixture::default()
}
}
fn discard_errors(fixture: Fixture) -> Vec<String> {
validate_fixtures_semantic(&[fixture], &make_e2e_config(), &["rust".to_string()])
.into_iter()
.filter(|error| error.severity == Severity::Error && error.message.contains("preserve_input_urls"))
.map(|error| error.message)
.collect()
}
#[test]
fn scheme_carrying_scalar_url_without_preserve_is_an_error() {
let fixture = url_fixture(
"mock_url",
"url",
serde_json::json!({"url": "https://target.example/a"}),
);
let messages = discard_errors(fixture);
assert_eq!(
messages.len(),
1,
"a declared absolute URL that codegen discards must be reported exactly once: {messages:?}"
);
assert!(
messages[0].contains("https://target.example/a"),
"the diagnostic must name the discarded literal: {messages:?}"
);
}
#[test]
fn scheme_carrying_url_list_without_preserve_is_an_error() {
let fixture = url_fixture(
"mock_url_list",
"urls",
serde_json::json!({"urls": ["https://target.example/a"]}),
);
let messages = discard_errors(fixture);
assert_eq!(
messages.len(),
1,
"a declared absolute URL list that codegen discards must be reported: {messages:?}"
);
}
#[test]
fn a_mock_server_relative_path_is_not_flagged() {
let fixture = url_fixture("mock_url", "url", serde_json::json!({"url": "/seed1"}));
assert!(
discard_errors(fixture).is_empty(),
"a bare path has no address of its own and is meant to resolve against the mock server"
);
}
#[test]
fn a_mock_url_placeholder_is_not_flagged() {
let fixture = url_fixture("mock_url", "url", serde_json::json!({"url": "$mock_url"}));
assert!(
discard_errors(fixture).is_empty(),
"the $mock_url placeholder asks for the mock server address by name"
);
}
#[test]
fn setting_preserve_input_urls_silences_the_check() {
let mut fixture = url_fixture(
"mock_url",
"url",
serde_json::json!({"url": "https://target.example/a"}),
);
fixture.preserve_input_urls = true;
assert!(
discard_errors(fixture).is_empty(),
"opting in is exactly the fix the diagnostic asks for"
);
}
#[test]
fn a_call_with_no_url_argument_is_not_flagged() {
let fixture = url_fixture(
"string",
"text",
serde_json::json!({"text": "https://target.example/a"}),
);
assert!(
discard_errors(fixture).is_empty(),
"a non-URL argument is passed through verbatim; nothing is discarded"
);
}
#[test]
fn preserve_input_urls_requires_mock_url_argument() {
let mut fixture = url_fixture("string", "text", serde_json::json!({"text": "sample"}));
fixture.preserve_input_urls = true;
let errors = validate_fixtures_semantic(&[fixture], &make_e2e_config(), &["rust".to_string()]);
assert!(errors.iter().any(|error| error.message.contains("flag has no effect")));
}
#[test]
fn preserve_input_urls_accepts_scalar_and_list_url_arguments() {
for arg_type in ["mock_url", "mock_url_list"] {
let mut fixture = url_fixture(arg_type, "urls", serde_json::json!({}));
fixture.preserve_input_urls = true;
let errors = validate_fixtures_semantic(&[fixture], &make_e2e_config(), &["rust".to_string()]);
assert!(!errors.iter().any(|error| error.message.contains("flag has no effect")));
}
}
#[test]
fn the_default_call_is_used_when_the_fixture_names_none() {
let mut fixture = url_fixture(
"mock_url",
"url",
serde_json::json!({"url": "https://target.example/a"}),
);
fixture.call = None;
let config = E2eConfig {
call: CallConfig::default(),
..E2eConfig::default()
};
let errors: Vec<_> = validate_fixtures_semantic(&[fixture], &config, &["rust".to_string()])
.into_iter()
.filter(|error| error.message.contains("preserve_input_urls"))
.collect();
assert_eq!(errors.len(), 1, "fixture-level args apply to the default call too");
}
}