use http::Method;
use parlov_core::{
Applicability, NormativeStrength, OracleClass, ResponseSurface, SignalSurface, Technique,
Vector,
};
use crate::strategy::Strategy;
use crate::types::{ProbeSpec, RiskLevel, StrategyMetadata};
use crate::util::{build_pair, clone_headers_static, json_body};
use crate::ScanContext;
static METADATA: StrategyMetadata = StrategyMetadata {
strategy_id: "uniqueness-elicit",
strategy_name: "Uniqueness Elicitation",
risk: RiskLevel::OperationDestructive,
};
fn uniqueness_applicable(baseline: &ResponseSurface, probe: &ResponseSurface) -> Applicability {
let is_strong = |s: u16| matches!(s, 409 | 422);
if is_strong(baseline.status.as_u16()) || is_strong(probe.status.as_u16()) {
return Applicability::Strong;
}
Applicability::Weak
}
static TECHNIQUE: Technique = Technique {
id: "uniqueness",
name: "Uniqueness conflict probe",
oracle_class: OracleClass::Existence,
vector: Vector::StatusCodeDiff,
strength: NormativeStrength::May,
normalization_weight: Some(0.05),
inverted_signal_weight: None,
method_relevant: false,
parser_relevant: true,
applicability: uniqueness_applicable,
contradiction_surface: SignalSurface::Status,
};
pub struct UniquenessElicitation;
impl Strategy for UniquenessElicitation {
fn metadata(&self) -> &'static StrategyMetadata {
&METADATA
}
fn technique_def(&self) -> &'static Technique {
&TECHNIQUE
}
fn methods(&self) -> &[Method] {
&[Method::POST, Method::PUT]
}
fn is_applicable(&self, ctx: &ScanContext) -> bool {
ctx.known_duplicate.is_some()
}
fn generate(&self, ctx: &ScanContext) -> Vec<ProbeSpec> {
let Some(kd) = &ctx.known_duplicate else {
return vec![];
};
let body = Some(json_body(&[(&kd.field, &kd.value)]));
let headers = clone_headers_static(&ctx.headers, "content-type", "application/json");
let mut specs = Vec::with_capacity(2);
for method in [Method::POST, Method::PUT] {
let pair = build_pair(
ctx,
method,
headers.clone(),
headers.clone(),
body.clone(),
METADATA.clone(),
TECHNIQUE,
);
specs.push(ProbeSpec::Pair(pair));
}
specs
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{ctx_operation_destructive, ctx_with_duplicate};
#[test]
fn risk_is_operation_destructive() {
assert_eq!(
UniquenessElicitation.risk(),
RiskLevel::OperationDestructive
);
}
#[test]
fn is_applicable_false_when_known_duplicate_none() {
assert!(!UniquenessElicitation.is_applicable(&ctx_operation_destructive()));
}
#[test]
fn is_applicable_true_when_known_duplicate_some() {
assert!(UniquenessElicitation.is_applicable(&ctx_with_duplicate()));
}
#[test]
fn generate_returns_two_items_when_applicable() {
assert_eq!(
UniquenessElicitation.generate(&ctx_with_duplicate()).len(),
2
);
}
#[test]
fn probe_body_contains_duplicate_field() {
let specs = UniquenessElicitation.generate(&ctx_with_duplicate());
let ProbeSpec::Pair(pair) = &specs[0] else {
panic!("expected Pair")
};
let body = pair.probe.body.as_deref().expect("body must be present");
let parsed: serde_json::Value = serde_json::from_slice(body).expect("valid JSON");
assert_eq!(parsed["email"], "alice@example.com");
}
#[test]
fn technique_strength_is_may() {
let specs = UniquenessElicitation.generate(&ctx_with_duplicate());
assert_eq!(specs[0].technique().strength, NormativeStrength::May);
}
#[test]
fn normalization_weight_is_0_05() {
assert_eq!(TECHNIQUE.normalization_weight, Some(0.05));
}
#[test]
fn inverted_signal_weight_is_none() {
assert_eq!(TECHNIQUE.inverted_signal_weight, None);
}
}