use super::*;
use crate::context::ScanContext;
use crate::types::RiskLevel;
use parlov_core::{always_applicable, NormativeStrength, OracleClass, SignalSurface, Vector};
fn make_metadata() -> StrategyMetadata {
StrategyMetadata {
strategy_id: "test",
strategy_name: "Test",
risk: RiskLevel::Safe,
}
}
fn make_technique() -> Technique {
Technique {
id: "test",
name: "Test technique",
oracle_class: OracleClass::Existence,
vector: Vector::StatusCodeDiff,
strength: NormativeStrength::Should,
normalization_weight: Some(0.2),
inverted_signal_weight: None,
method_relevant: false,
parser_relevant: false,
applicability: always_applicable,
contradiction_surface: SignalSurface::Status,
}
}
fn make_ctx() -> ScanContext {
ScanContext {
target: "https://api.example.com/users/{id}".to_owned(),
baseline_id: "1001".to_owned(),
probe_id: "9999".to_owned(),
headers: HeaderMap::new(),
max_risk: RiskLevel::Safe,
known_duplicate: None,
state_field: None,
alt_credential: None,
body_template: None,
}
}
#[test]
fn substitute_url_replaces_placeholder() {
let result = substitute_url("https://api.example.com/users/{id}", "1001");
assert_eq!(result, "https://api.example.com/users/1001");
}
#[test]
fn substitute_url_replaces_only_first_occurrence() {
let result = substitute_url("https://api.example.com/{id}/things/{id}", "42");
assert_eq!(result, "https://api.example.com/42/things/{id}");
}
#[test]
fn substitute_url_no_placeholder_returns_unchanged() {
let template = "https://api.example.com/users/all";
let result = substitute_url(template, "99");
assert_eq!(result, template);
}
#[test]
fn build_pair_baseline_url_uses_baseline_id() {
let ctx = make_ctx();
let pair = build_pair(
&ctx,
Method::GET,
HeaderMap::new(),
HeaderMap::new(),
None,
make_metadata(),
make_technique(),
);
assert_eq!(pair.baseline.url, "https://api.example.com/users/1001");
}
#[test]
fn build_pair_probe_url_uses_probe_id() {
let ctx = make_ctx();
let pair = build_pair(
&ctx,
Method::GET,
HeaderMap::new(),
HeaderMap::new(),
None,
make_metadata(),
make_technique(),
);
assert_eq!(pair.probe.url, "https://api.example.com/users/9999");
}
#[test]
fn build_pair_method_set_on_both_definitions() {
let ctx = make_ctx();
let pair = build_pair(
&ctx,
Method::POST,
HeaderMap::new(),
HeaderMap::new(),
None,
make_metadata(),
make_technique(),
);
assert_eq!(pair.baseline.method, Method::POST);
assert_eq!(pair.probe.method, Method::POST);
}
#[test]
fn build_pair_headers_assigned_to_correct_side() {
let ctx = make_ctx();
let baseline_hdrs = clone_headers_static(&HeaderMap::new(), "x-role", "admin");
let probe_hdrs = clone_headers_static(&HeaderMap::new(), "x-role", "guest");
let pair = build_pair(
&ctx,
Method::GET,
baseline_hdrs,
probe_hdrs,
None,
make_metadata(),
make_technique(),
);
assert_eq!(pair.baseline.headers.get("x-role").unwrap(), "admin");
assert_eq!(pair.probe.headers.get("x-role").unwrap(), "guest");
}
#[test]
fn build_pair_body_none_when_none() {
let ctx = make_ctx();
let pair = build_pair(
&ctx,
Method::GET,
HeaderMap::new(),
HeaderMap::new(),
None,
make_metadata(),
make_technique(),
);
assert!(pair.baseline.body.is_none());
assert!(pair.probe.body.is_none());
}
#[test]
fn build_pair_body_present_when_some() {
let ctx = make_ctx();
let body = Bytes::from_static(b"{\"x\":1}");
let pair = build_pair(
&ctx,
Method::POST,
HeaderMap::new(),
HeaderMap::new(),
Some(body.clone()),
make_metadata(),
make_technique(),
);
assert_eq!(pair.baseline.body.as_ref().unwrap(), &body);
assert_eq!(pair.probe.body.as_ref().unwrap(), &body);
}
#[test]
fn build_pair_body_template_substituted_when_no_explicit_body() {
let ctx = ScanContext {
body_template: Some(r#"{"id":"{id}"}"#.to_owned()),
..make_ctx()
};
let pair = build_pair(
&ctx,
Method::POST,
HeaderMap::new(),
HeaderMap::new(),
None,
make_metadata(),
make_technique(),
);
assert_eq!(pair.baseline.body.unwrap(), Bytes::from(r#"{"id":"1001"}"#));
assert_eq!(pair.probe.body.unwrap(), Bytes::from(r#"{"id":"9999"}"#));
}
#[test]
fn build_pair_explicit_body_overrides_template() {
let ctx = ScanContext {
body_template: Some(r#"{"id":"{id}"}"#.to_owned()),
..make_ctx()
};
let explicit = Bytes::from_static(b"explicit");
let pair = build_pair(
&ctx,
Method::POST,
HeaderMap::new(),
HeaderMap::new(),
Some(explicit.clone()),
make_metadata(),
make_technique(),
);
assert_eq!(pair.baseline.body.unwrap(), explicit);
assert_eq!(pair.probe.body.unwrap(), explicit);
}
#[test]
fn substitute_body_none_template_returns_none() {
assert!(substitute_body(None, "42").is_none());
}
#[test]
fn substitute_body_replaces_placeholder() {
let result = substitute_body(Some(r#"{"id":"{id}"}"#), "42").unwrap();
assert_eq!(result, Bytes::from(r#"{"id":"42"}"#));
}
#[test]
fn substitute_body_no_placeholder_returns_template_unchanged() {
let result = substitute_body(Some(r#"{"static":true}"#), "99").unwrap();
assert_eq!(result, Bytes::from(r#"{"static":true}"#));
}
#[test]
fn clone_headers_with_new_header_present() {
let result = clone_headers_static(&HeaderMap::new(), "x-custom", "hello");
assert_eq!(result.get("x-custom").unwrap(), "hello");
}
#[test]
fn clone_headers_with_preserves_existing_headers() {
let base = clone_headers_static(&HeaderMap::new(), "authorization", "Bearer tok");
let result = clone_headers_static(&base, "x-custom", "hello");
assert_eq!(result.get("authorization").unwrap(), "Bearer tok");
assert_eq!(result.get("x-custom").unwrap(), "hello");
}
#[test]
fn clone_headers_with_overwrites_existing_key() {
let base = clone_headers_static(&HeaderMap::new(), "x-role", "user");
let result = clone_headers_static(&base, "x-role", "admin");
assert_eq!(result.get("x-role").unwrap(), "admin");
assert_eq!(result.get_all("x-role").iter().count(), 1);
}
#[test]
fn try_clone_headers_with_valid_value_returns_some() {
let result = try_clone_headers_with(&HeaderMap::new(), "x-etag", "\"abc123\"");
assert!(result.is_some());
assert_eq!(result.unwrap().get("x-etag").unwrap(), "\"abc123\"");
}
#[test]
fn try_clone_headers_with_crlf_returns_none() {
let result = try_clone_headers_with(&HeaderMap::new(), "x-etag", "abc\r\nInjected: true");
assert!(result.is_none());
}
#[test]
fn try_clone_headers_with_nul_returns_none() {
let result = try_clone_headers_with(&HeaderMap::new(), "x-etag", "abc\0def");
assert!(result.is_none());
}
#[test]
fn try_clone_headers_with_preserves_existing_headers() {
let mut base = HeaderMap::new();
base.insert(
http::header::AUTHORIZATION,
http::HeaderValue::from_static("Bearer tok"),
);
let result = try_clone_headers_with(&base, "x-etag", "\"v1\"").unwrap();
assert_eq!(result.get("authorization").unwrap(), "Bearer tok");
assert_eq!(result.get("x-etag").unwrap(), "\"v1\"");
}
#[test]
fn garble_path_segment_multi_segment_url() {
let result = garble_path_segment("https://api.example.com/api/users/{id}");
assert_eq!(result, "https://api.example.com/api/_parlov_no_route/0");
}
#[test]
fn garble_path_segment_preserves_query_string() {
let result = garble_path_segment("https://api.example.com/api/users/{id}?version=2");
assert!(
result.contains("?version=2"),
"query string must be preserved, got: {result}"
);
assert!(
result.contains("_parlov_no_route"),
"must be garbled, got: {result}"
);
}
#[test]
fn garble_path_segment_single_segment() {
let result = garble_path_segment("https://api.example.com/{id}");
assert!(result.contains("_parlov_no_route"), "got: {result}");
assert!(
!result.contains("{id}"),
"placeholder must be gone, got: {result}"
);
}
#[test]
fn garble_path_segment_scheme_less_url() {
let result = garble_path_segment("/api/users/{id}");
assert!(result.contains("_parlov_no_route"), "got: {result}");
assert!(
!result.contains("{id}"),
"placeholder must be gone, got: {result}"
);
}
#[test]
fn garble_path_segment_url_with_port() {
let result = garble_path_segment("http://localhost:8080/api/users/{id}");
assert!(result.contains("_parlov_no_route"), "got: {result}");
assert!(!result.contains("{id}"), "got: {result}");
}
#[test]
fn json_body_single_field() {
let body = json_body(&[("status", "active")]);
let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(parsed["status"], "active");
}
#[test]
fn json_body_multiple_fields() {
let body = json_body(&[("email", "alice@example.com"), ("role", "admin")]);
let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(parsed["email"], "alice@example.com");
assert_eq!(parsed["role"], "admin");
}
#[test]
fn json_body_empty_slice_produces_empty_object() {
let body = json_body(&[]);
let parsed: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(parsed.as_object().unwrap().is_empty());
}
#[test]
fn json_body_roundtrips_through_serde() {
let body = json_body(&[("k", "v")]);
let result = serde_json::from_slice::<serde_json::Value>(&body);
assert!(result.is_ok());
}
#[test]
fn derive_probe_location_replaces_id_at_template_anchor() {
let result = derive_probe_location(
"https://api.example.com/users/{id}",
"123",
"999",
"https://api.example.com/users/123/",
);
assert_eq!(
result,
Some("https://api.example.com/users/999/".to_owned())
);
}
#[test]
fn derive_probe_location_returns_none_when_prefix_diverges() {
let result = derive_probe_location(
"https://api.example.com/users/{id}",
"123",
"999",
"https://other.example.com/users/123/",
);
assert!(result.is_none());
}
#[test]
fn derive_probe_location_returns_none_when_id_not_at_anchor() {
let result = derive_probe_location(
"https://api.example.com/users/{id}",
"123",
"999",
"https://api.example.com/users/456/",
);
assert!(result.is_none());
}
#[test]
fn derive_probe_location_does_not_substitute_id_in_version_segment() {
let result = derive_probe_location(
"https://api.example.com/v1/users/{id}",
"123",
"999",
"https://api.example.com/v1/users/123/",
);
assert_eq!(
result,
Some("https://api.example.com/v1/users/999/".to_owned())
);
}
#[test]
fn derive_probe_location_does_not_corrupt_port_containing_id() {
let result = derive_probe_location(
"http://127.0.0.1:8001/users/{id}",
"1",
"9",
"http://127.0.0.1:8001/users/1/",
);
assert_eq!(result, Some("http://127.0.0.1:8001/users/9/".to_owned()));
}
#[test]
fn derive_probe_location_returns_none_when_no_id_placeholder_in_target() {
let result = derive_probe_location(
"https://api.example.com/users/static",
"123",
"999",
"https://api.example.com/users/static/",
);
assert!(result.is_none());
}