use super::*;
use crate::types::RiskLevel;
use crate::{KnownDuplicate, ScanContext, StateField};
use http::{HeaderMap, HeaderValue};
use parlov_core::Vector;
pub(super) fn make_minimal_ctx(max_risk: RiskLevel) -> ScanContext {
ScanContext {
target: "https://api.example.com/users/{id}".to_string(),
baseline_id: "1001".to_string(),
probe_id: "9999".to_string(),
headers: HeaderMap::new(),
max_risk,
known_duplicate: None,
state_field: None,
alt_credential: None,
body_template: None,
}
}
pub(super) fn make_full_ctx() -> ScanContext {
let mut headers = HeaderMap::new();
headers.insert(
http::header::AUTHORIZATION,
HeaderValue::from_static("Bearer token123"),
);
let mut alt = HeaderMap::new();
alt.insert(
http::header::AUTHORIZATION,
HeaderValue::from_static("Bearer under-scoped"),
);
ScanContext {
target: "https://api.example.com/users/{id}".to_string(),
baseline_id: "1001".to_string(),
probe_id: "9999".to_string(),
headers,
max_risk: RiskLevel::OperationDestructive,
known_duplicate: Some(KnownDuplicate {
field: "email".to_string(),
value: "alice@example.com".to_string(),
}),
state_field: Some(StateField {
field: "status".to_string(),
value: "invalid_state".to_string(),
}),
alt_credential: Some(alt),
body_template: None,
}
}
pub(super) fn spec_strategy_id(spec: &ProbeSpec) -> &str {
match spec {
ProbeSpec::Pair(p) | ProbeSpec::HeaderDiff(p) => p.metadata.strategy_id,
ProbeSpec::Burst(b) => b.metadata.strategy_id,
}
}
#[test]
fn all_strategies_returns_exactly_39() {
assert_eq!(all_strategies().len(), 39);
}
#[test]
fn risk_distribution_is_26_10_3() {
let s = all_strategies();
let count = |r: RiskLevel| s.iter().filter(|x| x.risk() == r).count();
assert_eq!(count(RiskLevel::Safe), 26);
assert_eq!(count(RiskLevel::MethodDestructive), 10);
assert_eq!(count(RiskLevel::OperationDestructive), 3);
}
#[test]
fn all_strategy_ids_are_unique() {
let strategies = all_strategies();
let mut ids: Vec<&str> = strategies.iter().map(|s| s.id()).collect();
ids.sort_unstable();
let before = ids.len();
ids.dedup();
assert_eq!(before, ids.len(), "duplicate strategy IDs detected");
}
#[test]
fn safe_filter_excludes_method_and_op_destructive() {
let ctx = make_minimal_ctx(RiskLevel::Safe);
let plan = generate_plan(&ctx);
let safe_ids: std::collections::HashSet<&str> = all_strategies()
.iter()
.filter(|s| s.risk() == RiskLevel::Safe)
.map(|s| s.id())
.collect();
for spec in &plan {
assert!(
safe_ids.contains(spec_strategy_id(spec)),
"non-Safe strategy '{}' appeared in Safe plan",
spec_strategy_id(spec)
);
}
}
#[test]
fn method_destructive_ceiling_excludes_op_destructive() {
let ctx = make_minimal_ctx(RiskLevel::MethodDestructive);
let plan = generate_plan(&ctx);
for spec in &plan {
assert_ne!(
spec_strategy_id(spec),
UniquenessElicitation.id(),
"OperationDestructive strategy appeared in MethodDestructive plan"
);
assert_ne!(
spec_strategy_id(spec),
DependencyDeleteElicitation.id(),
"OperationDestructive strategy appeared in MethodDestructive plan"
);
assert_ne!(
spec_strategy_id(spec),
RateLimitBurstElicitation.id(),
"OperationDestructive strategy appeared in MethodDestructive plan"
);
}
}
#[test]
fn no_auth_excludes_auth_strip_and_low_privilege() {
let ctx = make_minimal_ctx(RiskLevel::OperationDestructive);
let plan = generate_plan(&ctx);
for spec in &plan {
assert_ne!(spec_strategy_id(spec), AuthStripElicitation.id());
assert_ne!(spec_strategy_id(spec), LowPrivilegeElicitation.id());
}
}
#[test]
fn no_alt_credential_excludes_scope_manipulation() {
let ctx = make_minimal_ctx(RiskLevel::OperationDestructive);
let plan = generate_plan(&ctx);
for spec in &plan {
assert_ne!(spec_strategy_id(spec), ScopeManipulationElicitation.id());
}
}
#[test]
fn no_state_field_excludes_state_transition() {
let ctx = make_minimal_ctx(RiskLevel::MethodDestructive);
let plan = generate_plan(&ctx);
for spec in &plan {
assert_ne!(spec_strategy_id(spec), StateTransitionElicitation.id());
}
}
#[test]
fn no_known_duplicate_excludes_uniqueness() {
let ctx = make_minimal_ctx(RiskLevel::OperationDestructive);
let plan = generate_plan(&ctx);
for spec in &plan {
assert_ne!(spec_strategy_id(spec), UniquenessElicitation.id());
}
}
#[test]
fn full_plan_returns_at_least_37_specs() {
let ctx = make_full_ctx();
let plan = generate_plan(&ctx);
assert!(plan.len() >= 37, "expected >= 37 specs, got {}", plan.len());
}
#[test]
fn applicable_strategies_excludes_above_max_risk() {
let strategies = all_strategies();
let ctx = make_minimal_ctx(RiskLevel::Safe);
let applicable: Vec<_> = applicable_strategies(&strategies, &ctx).collect();
for s in &applicable {
assert!(
s.risk() <= RiskLevel::Safe,
"strategy '{}' with risk {:?} must not appear in Safe-capped result",
s.id(),
s.risk(),
);
}
let all_count = strategies.iter().filter(|s| s.is_applicable(&ctx)).count();
assert!(
applicable.len() < all_count || strategies.iter().any(|s| s.risk() > RiskLevel::Safe),
"expected some strategies to be excluded by risk cap",
);
}
#[test]
fn applicable_strategies_excludes_inapplicable() {
let strategies = all_strategies();
let ctx = make_minimal_ctx(RiskLevel::OperationDestructive);
let applicable: Vec<_> = applicable_strategies(&strategies, &ctx).collect();
let ids: Vec<&str> = applicable.iter().map(|s| s.id()).collect();
assert!(
!ids.contains(&AuthStripElicitation.id()),
"inapplicable AuthStripElicitation must be excluded",
);
assert!(
!ids.contains(&LowPrivilegeElicitation.id()),
"inapplicable LowPrivilegeElicitation must be excluded",
);
}
#[test]
fn applicable_strategies_both_filters_applied_simultaneously() {
let strategies = all_strategies();
let ctx = make_minimal_ctx(RiskLevel::Safe);
let applicable: Vec<_> = applicable_strategies(&strategies, &ctx).collect();
for s in &applicable {
assert!(
s.risk() <= ctx.max_risk,
"risk filter violated for '{}'",
s.id()
);
assert!(
s.is_applicable(&ctx),
"applicability filter violated for '{}'",
s.id()
);
}
}
#[test]
fn all_specs_have_known_vector() {
let ctx = make_full_ctx();
let plan = generate_plan(&ctx);
for spec in &plan {
let v = spec.technique().vector;
assert!(
v == Vector::StatusCodeDiff
|| v == Vector::CacheProbing
|| v == Vector::ErrorMessageGranularity
|| v == Vector::RedirectDiff,
"strategy '{}' has unexpected vector: {:?}",
spec_strategy_id(spec),
v,
);
}
}
#[test]
fn all_specs_have_populated_technique_id() {
let ctx = make_full_ctx();
let plan = generate_plan(&ctx);
for spec in &plan {
assert!(
!spec.technique().id.is_empty(),
"strategy '{}' has empty technique id",
spec_strategy_id(spec),
);
}
}
#[cfg(test)]
#[path = "registry_report_tests.rs"]
mod report;