use security_rust::session::{
Decision, MemoryStore, RequestContext, SessionConfig, SessionGuard, SessionThreat,
};
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())
}
#[test]
fn bind_first_login_from_new_region_is_allowed() {
let g = guard();
let v = g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
assert!(v.is_allowed());
assert!(v.threats.is_empty());
}
#[test]
fn bind_from_different_region_than_history_challenges() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
let mut c = ctx("t2", "u1", FP);
c.location = Some("US-NY");
c.coords = None;
let v = g.bind(&c, NOW + 86_400).unwrap();
assert_eq!(v.decision, Decision::Challenge);
assert!(v.threats.contains(&SessionThreat::LocationChanged));
}
#[test]
fn bind_same_region_case_insensitive_is_allowed() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
let mut c = ctx("t2", "u1", FP);
c.location = Some("cn-bj");
let v = g.bind(&c, NOW + 86_400).unwrap();
assert!(v.is_allowed(), "大小写差异不该报异地: {:?}", v.threats);
}
#[test]
fn bind_impossible_travel_blocks() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
let mut c = ctx("t2", "u1", FP);
c.location = Some("US-NY");
c.coords = Some((40.7128, -74.0060));
let v = g.bind(&c, NOW + 3_600).unwrap();
assert_eq!(v.decision, Decision::Block);
assert!(
v.threats
.iter()
.any(|t| matches!(t, SessionThreat::ImpossibleTravel { .. })),
"got {:?}",
v.threats
);
}
#[test]
fn bind_does_not_compare_against_its_own_login_point() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
let v = g.bind(&ctx("t2", "u1", FP), NOW + 60).unwrap();
assert!(v.is_allowed(), "同地登录误报: {:?}", v.threats);
}
#[test]
fn bind_rejects_empty_token() {
let g = guard();
assert_eq!(
g.bind(&ctx("", "u1", FP), NOW).unwrap_err(),
security_rust::session::SessionError::EmptyToken
);
}
#[test]
fn bind_rejects_empty_subject() {
let g = guard();
assert_eq!(
g.bind(&ctx("t1", "", FP), NOW).unwrap_err(),
security_rust::session::SessionError::EmptySubject
);
}
#[test]
fn bind_rejects_empty_fingerprint() {
let g = guard();
assert_eq!(
g.bind(&ctx("t1", "u1", ""), NOW).unwrap_err(),
security_rust::session::SessionError::EmptyFingerprint
);
}
#[test]
fn revoke_all_kills_every_session_of_subject() {
let g = guard();
g.bind(&ctx("t1", "u1", FP), NOW).unwrap();
g.bind(&ctx("t2", "u1", FP), NOW).unwrap();
g.bind(&ctx("t3", "u2", "fp2"), NOW).unwrap();
assert_eq!(g.revoke_all("u1").unwrap(), 2);
assert_eq!(
g.verify(&ctx("t1", "u1", FP), NOW + 10).threats,
vec![SessionThreat::TokenRevoked]
);
assert_eq!(
g.verify(&ctx("t2", "u1", FP), NOW + 10).threats,
vec![SessionThreat::TokenRevoked]
);
assert!(g.verify(&ctx("t3", "u2", "fp2"), NOW + 10).is_allowed());
}
#[test]
fn rotate_rejects_unknown_old_token() {
let g = guard();
assert_eq!(
g.rotate("ghost", "new", &ctx("ghost", "u1", FP), NOW)
.unwrap_err(),
security_rust::session::SessionError::UnknownSession
);
}
#[test]
fn rotate_rejects_expired_old_token() {
let g = guard();
g.bind(&ctx("old", "u1", FP), NOW).unwrap();
assert_eq!(
g.rotate("old", "new", &ctx("old", "u1", FP), NOW + 3_601)
.unwrap_err(),
security_rust::session::SessionError::UnknownSession
);
}
#[test]
fn rotate_rejects_revoked_old_token() {
let g = guard();
g.bind(&ctx("old", "u1", FP), NOW).unwrap();
g.revoke("old").unwrap();
assert_eq!(
g.rotate("old", "new", &ctx("old", "u1", FP), NOW + 10)
.unwrap_err(),
security_rust::session::SessionError::UnknownSession
);
}
#[test]
fn rotate_rejects_empty_new_token() {
let g = guard();
g.bind(&ctx("old", "u1", FP), NOW).unwrap();
assert_eq!(
g.rotate("old", "", &ctx("old", "u1", FP), NOW + 10)
.unwrap_err(),
security_rust::session::SessionError::EmptyToken
);
}
#[test]
fn rotate_rejects_fingerprint_mismatch() {
let g = guard();
g.bind(&ctx("old", "u1", FP), NOW).unwrap();
assert_eq!(
g.rotate("old", "new", &ctx("old", "u1", "ATTACKER"), NOW + 10)
.unwrap_err(),
security_rust::session::SessionError::UnknownSession
);
}
#[test]
fn full_session_lifecycle() {
let g = SessionGuard::new(MemoryStore::new(), SessionConfig::default());
let v = g.bind(&ctx("tok-1", "alice", FP), NOW).unwrap();
assert!(v.is_allowed());
let ok = g.verify(&ctx("tok-1", "alice", FP), NOW + 60);
assert!(ok.is_allowed(), "误报: {:?}", ok.threats);
let hijack = g.verify(&ctx("tok-1", "alice", "ip=9.9.9.9|ua=evil"), NOW + 120);
assert_eq!(hijack.decision, Decision::Block);
assert!(hijack.threats.contains(&SessionThreat::FingerprintMismatch));
g.rotate("tok-1", "tok-2", &ctx("tok-1", "alice", FP), NOW + 180)
.unwrap();
assert_eq!(
g.verify(&ctx("tok-1", "alice", FP), NOW + 240).threats,
vec![SessionThreat::TokenRevoked]
);
assert!(g.verify(&ctx("tok-2", "alice", FP), NOW + 240).is_allowed());
assert_eq!(g.revoke_all("alice").unwrap(), 1);
assert_eq!(
g.verify(&ctx("tok-2", "alice", FP), NOW + 300).threats,
vec![SessionThreat::TokenRevoked]
);
}