use security_rust::Severity;
use security_rust::session::store::{LoginPoint, SessionRecord, SessionStore};
use security_rust::session::{
Decision, MemoryStore, RequestContext, SessionConfig, SessionGuard, SessionThreat, StoreError,
};
const NOW: u64 = 1_000_000;
const FP: &str = "ip=1.2.3.4|ua=curl";
fn ctx<'a>(token: &'a str, subject: &'a str, fp: &'a str) -> RequestContext<'a> {
RequestContext {
token,
subject,
fingerprint: fp,
location: Some("CN-BJ"),
coords: Some((39.9042, 116.4074)),
signature: None,
at: None,
}
}
fn guard() -> SessionGuard<MemoryStore> {
SessionGuard::new(MemoryStore::new(), SessionConfig::default())
}
struct BrokenStore;
impl SessionStore for BrokenStore {
fn put(&self, _r: SessionRecord) -> Result<(), StoreError> {
Err(StoreError::Unavailable)
}
fn get(&self, _t: &str) -> Result<Option<SessionRecord>, StoreError> {
Err(StoreError::Unavailable)
}
fn touch(&self, _t: &str, _n: u64) -> Result<(), StoreError> {
Err(StoreError::Unavailable)
}
fn revoke(&self, _t: &str) -> Result<(), StoreError> {
Err(StoreError::Unavailable)
}
fn revoke_subject(&self, _s: &str) -> Result<usize, StoreError> {
Err(StoreError::Unavailable)
}
fn recent_logins(&self, _s: &str) -> Result<Vec<LoginPoint>, StoreError> {
Err(StoreError::Unavailable)
}
fn record_login(&self, _s: &str, _p: LoginPoint) -> Result<(), StoreError> {
Err(StoreError::Unavailable)
}
fn purge_expired(&self, _n: u64) -> Result<usize, StoreError> {
Err(StoreError::Unavailable)
}
}
struct HalfBrokenStore(MemoryStore);
impl SessionStore for HalfBrokenStore {
fn put(&self, r: SessionRecord) -> Result<(), StoreError> {
self.0.put(r)
}
fn get(&self, t: &str) -> Result<Option<SessionRecord>, StoreError> {
self.0.get(t)
}
fn touch(&self, t: &str, n: u64) -> Result<(), StoreError> {
self.0.touch(t, n)
}
fn revoke(&self, t: &str) -> Result<(), StoreError> {
self.0.revoke(t)
}
fn revoke_subject(&self, s: &str) -> Result<usize, StoreError> {
self.0.revoke_subject(s)
}
fn recent_logins(&self, _s: &str) -> Result<Vec<LoginPoint>, StoreError> {
Err(StoreError::Corrupt)
}
fn record_login(&self, _s: &str, _p: LoginPoint) -> Result<(), StoreError> {
Err(StoreError::Corrupt)
}
fn purge_expired(&self, n: u64) -> Result<usize, StoreError> {
self.0.purge_expired(n)
}
}
fn record(token: &str, subject: &str) -> SessionRecord {
SessionRecord {
token: token.into(),
subject: subject.into(),
fingerprint: FP.into(),
location: Some("CN-BJ".into()),
coords: Some((39.9042, 116.4074)),
signature: None,
issued_at: NOW,
last_seen: NOW,
expires_at: NOW + 3_600,
revoked: false,
}
}
#[test]
fn verify_unknown_token_blocks() {
let g = guard();
let v = g.verify(&ctx("ghost", "u1", FP), NOW);
assert_eq!(v.decision, Decision::Block);
assert_eq!(v.threats, vec![SessionThreat::TokenUnknown]);
assert_eq!(v.severity, Some(Severity::Critical));
}
#[test]
fn verify_empty_token_blocks_as_unknown() {
let g = guard();
let v = g.verify(&ctx("", "u1", FP), NOW);
assert_eq!(v.decision, Decision::Block);
assert_eq!(v.threats, vec![SessionThreat::TokenUnknown]);
}
#[test]
fn verify_revoked_token_blocks() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
g.revoke("t1").unwrap();
let v = g.verify(&ctx("t1", "u1", FP), NOW);
assert_eq!(v.decision, Decision::Block);
assert_eq!(v.threats, vec![SessionThreat::TokenRevoked]);
}
#[test]
fn verify_revoked_distinct_from_unknown() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
g.revoke("t1").unwrap();
let revoked = g.verify(&ctx("t1", "u1", FP), NOW);
let unknown = g.verify(&ctx("ghost", "u1", FP), NOW);
assert_ne!(revoked.threats, unknown.threats);
}
#[test]
fn verify_expired_token_blocks_with_low_severity() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
let v = g.verify(&ctx("t1", "u1", FP), NOW + 3_601);
assert_eq!(v.decision, Decision::Block);
assert_eq!(v.threats, vec![SessionThreat::TokenExpired]);
assert_eq!(v.severity, Some(Severity::Low), "过期是例行情况,非攻击");
}
#[test]
fn verify_expired_distinct_from_unknown() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
let expired = g.verify(&ctx("t1", "u1", FP), NOW + 3_601);
let unknown = g.verify(&ctx("ghost", "u1", FP), NOW + 3_601);
assert_ne!(expired.threats, unknown.threats);
}
#[test]
fn verify_boundary_expires_at_equals_now_is_expired() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
let v = g.verify(&ctx("t1", "u1", FP), NOW + 3_600);
assert_eq!(v.threats, vec![SessionThreat::TokenExpired]);
}
#[test]
fn verify_one_second_before_expiry_is_allowed() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
let v = g.verify(&ctx("t1", "u1", FP), NOW + 3_599);
assert!(v.is_allowed(), "got {:?}", v.threats);
}
#[test]
fn verify_valid_request_is_allowed() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
let v = g.verify(&ctx("t1", "u1", FP), NOW + 10);
assert!(v.is_allowed(), "误报: {:?}", v.threats);
assert!(v.threats.is_empty());
assert_eq!(
v.severity, None,
"放行时不该有 severity(更不该拿 LOW 占位)"
);
}
#[test]
fn verify_fingerprint_mismatch_is_hijack_block() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
let v = g.verify(&ctx("t1", "u1", "fp2"), NOW + 10);
assert_eq!(v.decision, Decision::Block);
assert_eq!(v.severity, Some(Severity::Critical));
assert_eq!(v.threats, vec![SessionThreat::FingerprintMismatch]);
}
#[test]
fn verify_signature_mismatch_is_tamper_block() {
let g = guard();
let mut c = ctx("t1", "u1", FP);
c.signature = Some("mac-original");
g.bind(&c, NOW).unwrap();
let mut c2 = ctx("t1", "u1", FP);
c2.signature = Some("mac-tampered");
let v = g.verify(&c2, NOW + 10);
assert_eq!(v.decision, Decision::Block);
assert!(v.threats.contains(&SessionThreat::SignatureInvalid));
}
#[test]
fn verify_signature_missing_when_baseline_had_one() {
let g = guard();
let mut c = ctx("t1", "u1", FP);
c.signature = Some("mac-original");
g.bind(&c, NOW).unwrap();
let v = g.verify(&ctx("t1", "u1", FP), NOW + 10); assert_eq!(v.decision, Decision::Block);
assert_eq!(v.threats, vec![SessionThreat::SignatureMissing]);
}
#[test]
fn verify_signature_unexpected_challenges() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
let mut c = ctx("t1", "u1", FP);
c.signature = Some("mac");
let v = g.verify(&c, NOW + 10);
assert_eq!(v.threats, vec![SessionThreat::SignatureUnexpected]);
assert_eq!(v.decision, Decision::Challenge);
assert_eq!(v.severity, Some(Severity::Medium));
}
#[test]
fn verify_matching_signature_allows() {
let g = guard();
let mut c = ctx("t1", "u1", FP);
c.signature = Some("mac-original");
g.bind(&c, NOW).unwrap();
let mut c2 = ctx("t1", "u1", FP);
c2.signature = Some("mac-original");
assert!(g.verify(&c2, NOW + 10).is_allowed());
}
#[test]
fn verify_no_signature_on_either_side_allows() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
assert!(g.verify(&ctx("t1", "u1", FP), NOW + 10).is_allowed());
}
#[test]
fn verify_timestamp_skew_challenges() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
let mut c = ctx("t1", "u1", FP);
c.at = Some(NOW + 3_601); let v = g.verify(&c, NOW);
assert_eq!(v.decision, Decision::Challenge);
assert!(v.threats.contains(&SessionThreat::TimestampSkew));
}
#[test]
fn verify_timestamp_within_skew_allows() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
let mut c = ctx("t1", "u1", FP);
c.at = Some(NOW + 10 + 299); assert!(g.verify(&c, NOW + 10).is_allowed());
}
#[test]
fn verify_absent_timestamp_skips_skew_check() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
assert!(g.verify(&ctx("t1", "u1", FP), NOW + 1_000).is_allowed());
}
#[test]
fn verify_location_change_challenges() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap(); let mut c = ctx("t1", "u1", FP);
c.location = Some("US-NY");
c.coords = None;
let v = g.verify(&c, NOW + 600);
assert_eq!(v.decision, Decision::Challenge);
assert!(v.threats.contains(&SessionThreat::LocationChanged));
assert!(
!v.threats
.iter()
.any(|t| matches!(t, SessionThreat::ImpossibleTravel { .. })),
"无坐标时不该报不可能旅行"
);
}
#[test]
fn verify_impossible_travel_blocks() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap(); let mut c = ctx("t1", "u1", FP);
c.location = Some("US-NY");
c.coords = Some((40.7128, -74.0060));
let v = g.verify(&c, NOW + 3_500);
assert_eq!(v.decision, Decision::Block);
assert!(
v.threats
.iter()
.any(|t| matches!(t, SessionThreat::ImpossibleTravel { .. })),
"got {:?}",
v.threats
);
}
#[test]
fn verify_missing_location_on_either_side_is_not_a_threat() {
let g = SessionGuard::new(MemoryStore::new(), SessionConfig::default());
let mut c = ctx("t1", "u1", FP);
c.location = None;
c.coords = None;
g.bind(&c, NOW).unwrap();
let mut c2 = ctx("t1", "u1", FP);
c2.location = Some("CN-BJ");
let v = g.verify(&c2, NOW + 10);
assert!(
!v.threats.contains(&SessionThreat::LocationChanged),
"记录侧无位置时不该报异地: {:?}",
v.threats
);
}
#[test]
fn verify_multiple_threats_accumulate() {
let g = guard();
let mut c = ctx("t1", "u1", FP);
c.signature = Some("mac-original");
g.bind(&c, NOW).unwrap();
let mut bad = ctx("t1", "u1", "DIFFERENT-FP");
bad.signature = Some("mac-tampered");
bad.location = Some("US-NY");
bad.coords = None;
bad.at = Some(NOW + 100_000);
let v = g.verify(&bad, NOW + 10);
assert!(v.threats.contains(&SessionThreat::FingerprintMismatch));
assert!(v.threats.contains(&SessionThreat::SignatureInvalid));
assert!(v.threats.contains(&SessionThreat::LocationChanged));
assert!(v.threats.contains(&SessionThreat::TimestampSkew));
assert_eq!(v.decision, Decision::Block, "Block 压过 Challenge");
assert_eq!(v.severity, Some(Severity::Critical));
}
#[test]
fn verify_fails_closed_when_store_unavailable() {
let g = SessionGuard::new(BrokenStore, SessionConfig::default());
let v = g.verify(&ctx("t1", "u1", FP), NOW);
assert_eq!(
v.decision,
Decision::Block,
"后端故障绝不能放行 —— 这是可被攻击者主动触发的绕过"
);
assert_eq!(v.threats, vec![SessionThreat::StoreUnavailable]);
}
#[test]
fn verify_fails_closed_when_login_history_unavailable() {
let store = HalfBrokenStore(MemoryStore::new());
store.put(record("t1", "u1")).unwrap();
let g = SessionGuard::new(store, SessionConfig::default());
let v = g.verify(&ctx("t1", "u1", FP), NOW + 10);
assert_eq!(v.decision, Decision::Block, "登录历史读不到时绝不能放行");
assert_eq!(v.threats, vec![SessionThreat::StoreUnavailable]);
}
#[test]
fn bind_propagates_store_failure_as_error() {
let g = SessionGuard::new(BrokenStore, SessionConfig::default());
assert!(g.bind(&ctx("t1", "u1", FP), NOW).is_err());
}