use super::*;
use parlov_elicit::RiskLevel;
use parlov_probe::http::HttpProbe;
#[test]
fn build_scan_context_target_matches_and_risk_defaults_to_safe() {
let args = minimal_args("https://api.example.com/users/{id}", "1001");
let ctx = build_scan_context(&args, &[]).unwrap();
assert_eq!(ctx.target, "https://api.example.com/users/{id}");
assert_eq!(ctx.max_risk, RiskLevel::Safe);
}
#[test]
fn spec_strategy_id_returns_pair_strategy_id() {
use http::{HeaderMap, Method};
use parlov_core::{
always_applicable, NormativeStrength, OracleClass, ProbeDefinition, SignalSurface,
Technique, Vector,
};
use parlov_elicit::{ProbePair, StrategyMetadata};
let pair = ProbePair {
baseline: ProbeDefinition {
url: "https://example.com/1".to_owned(),
method: Method::GET,
headers: HeaderMap::new(),
body: None,
},
probe: ProbeDefinition {
url: "https://example.com/2".to_owned(),
method: Method::GET,
headers: HeaderMap::new(),
body: None,
},
canonical_baseline: None,
metadata: StrategyMetadata {
strategy_id: "auth-strip-elicit",
strategy_name: "Auth Strip Elicitation",
risk: RiskLevel::Safe,
},
technique: Technique {
id: "auth-strip",
name: "Auth strip",
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,
},
chain_provenance: None,
};
let spec = ProbeSpec::Pair(pair);
assert_eq!(spec_strategy_id(&spec), "auth-strip-elicit");
}
#[test]
fn harvest_allowlist_admits_h1_h2_and_h3_classes() {
use http::{HeaderMap, HeaderValue, StatusCode};
use parlov_core::ResponseClass;
assert!(matches!(
ResponseClass::classify(StatusCode::OK, &HeaderMap::new()),
ResponseClass::Success
));
let mut redirect_headers = HeaderMap::new();
redirect_headers.insert(
http::header::LOCATION,
HeaderValue::from_static("https://example.com/new"),
);
assert!(matches!(
ResponseClass::classify(StatusCode::MOVED_PERMANENTLY, &redirect_headers),
ResponseClass::Redirect
));
assert!(matches!(
ResponseClass::classify(StatusCode::PARTIAL_CONTENT, &HeaderMap::new()),
ResponseClass::PartialContent
));
assert!(matches!(
ResponseClass::classify(StatusCode::RANGE_NOT_SATISFIABLE, &HeaderMap::new()),
ResponseClass::RangeNotSatisfiable
));
assert!(!matches!(
ResponseClass::classify(StatusCode::UNAUTHORIZED, &HeaderMap::new()),
ResponseClass::Success
| ResponseClass::Redirect
| ResponseClass::PartialContent
| ResponseClass::RangeNotSatisfiable
));
assert!(!matches!(
ResponseClass::classify(StatusCode::INTERNAL_SERVER_ERROR, &HeaderMap::new()),
ResponseClass::Success
| ResponseClass::Redirect
| ResponseClass::PartialContent
| ResponseClass::RangeNotSatisfiable
));
assert!(!matches!(
ResponseClass::classify(StatusCode::MOVED_PERMANENTLY, &HeaderMap::new()),
ResponseClass::Success
| ResponseClass::Redirect
| ResponseClass::PartialContent
| ResponseClass::RangeNotSatisfiable
));
}
#[tokio::test]
async fn run_plan_specs_returns_exchanges_on_empty_plan() {
use bytes::Bytes;
use parlov_core::ResponseClass;
let mut state = ScanPipelineState::new(0);
let stop_rule = StopRule::new();
let probe = HttpProbe::new();
let exchanges: Vec<(ResponseClass, http::HeaderMap, Bytes)> = run_plan_specs(
&[],
"https://example.com/{id}",
&mut state,
&stop_rule,
&probe,
crate::scan_exec::RunOpts {
exhaustive: false,
confirm_threshold: CONFIRM_LOG_ODDS_THRESHOLD,
repro: false,
verbose: false,
},
)
.await;
assert!(exchanges.is_empty());
assert!(state.findings.is_empty());
assert!(state.stop_decision.is_none());
}
#[test]
fn pipeline_state_strategies_total_accumulates_phase2() {
let mut state = ScanPipelineState::new(5);
assert_eq!(state.strategies_total, 5);
state.strategies_total += 3;
assert_eq!(
state.strategies_total, 8,
"strategies_total must accumulate phase-2 chained spec count"
);
}