#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod common;
use std::sync::Arc;
use std::time::Duration;
use common::MemorySessionRevocation;
use ppoppo_token::access_token::{AuthError, IssueConfig, IssueRequest, SessionRevocation, VerifyConfig, issue, verify};
use ppoppo_token::{SigningKey};
const TEST_SUB: &str = "01HSAB00000000000000000000";
const TEST_CLIENT_ID: &str = "ppoppo-internal";
const TEST_SID: &str = "01HSESSION0000000000000000";
const TTL_15M: Duration = Duration::from_secs(900);
const ISSUER: &str = "https://accounts.ppoppo.com";
const AUDIENCE: &str = "ppoppo";
fn mint_token_with_sid(signer: &SigningKey, sid: &str) -> String {
let issue_cfg = IssueConfig::access_token(ISSUER, AUDIENCE, signer.kid());
let req = IssueRequest::new(TEST_SUB, TEST_CLIENT_ID, TTL_15M).with_sid(sid);
issue(&req, &issue_cfg, signer, time::OffsetDateTime::now_utc().unix_timestamp()).expect("issue should succeed")
}
fn mint_token_without_sid(signer: &SigningKey) -> String {
let issue_cfg = IssueConfig::access_token(ISSUER, AUDIENCE, signer.kid());
let req = IssueRequest::new(TEST_SUB, TEST_CLIENT_ID, TTL_15M);
issue(&req, &issue_cfg, signer, time::OffsetDateTime::now_utc().unix_timestamp()).expect("issue should succeed")
}
#[tokio::test]
async fn session_admits_when_cfg_session_is_none() {
let (signer, key_set) = SigningKey::test_pair();
let token = mint_token_with_sid(&signer, TEST_SID);
let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE);
let claims = verify(&token, &cfg, &key_set, time::OffsetDateTime::now_utc().unix_timestamp()).await.expect("verify must admit");
assert_eq!(claims.sid.as_deref(), Some(TEST_SID), "sid must surface on Claims");
}
#[tokio::test]
async fn session_admits_when_token_has_no_sid() {
let (signer, key_set) = SigningKey::test_pair();
let token = mint_token_without_sid(&signer);
let port = Arc::new(MemorySessionRevocation::new());
let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE).with_session_revocation(port);
let claims = verify(&token, &cfg, &key_set, time::OffsetDateTime::now_utc().unix_timestamp()).await.expect("verify must admit");
assert!(claims.sid.is_none(), "sid must be None on Claims for non-session-bound token");
}
#[tokio::test]
async fn session_admits_active_sid() {
let (signer, key_set) = SigningKey::test_pair();
let token = mint_token_with_sid(&signer, TEST_SID);
let port = Arc::new(MemorySessionRevocation::new());
let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE).with_session_revocation(port);
let claims = verify(&token, &cfg, &key_set, time::OffsetDateTime::now_utc().unix_timestamp()).await.expect("active sid must admit");
assert_eq!(claims.sub, TEST_SUB);
assert_eq!(claims.sid.as_deref(), Some(TEST_SID));
}
#[tokio::test]
async fn session_rejects_revoked_sid_with_session_revoked() {
let (signer, key_set) = SigningKey::test_pair();
let token = mint_token_with_sid(&signer, TEST_SID);
let port = Arc::new(MemorySessionRevocation::new());
port.revoke(TEST_SUB, TEST_SID);
let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE).with_session_revocation(port);
let result = verify(&token, &cfg, &key_set, time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(
result,
Err(AuthError::SessionRevoked),
"revoked sid must reject as SessionRevoked",
);
}
#[tokio::test]
async fn session_revoke_does_not_affect_sibling_sessions() {
let (signer, key_set) = SigningKey::test_pair();
let other_sid = "01HSESSION9999999999999999";
let token_a = mint_token_with_sid(&signer, TEST_SID);
let token_b = mint_token_with_sid(&signer, other_sid);
let port = Arc::new(MemorySessionRevocation::new());
port.revoke(TEST_SUB, TEST_SID); let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE).with_session_revocation(port);
assert_eq!(
verify(&token_a, &cfg, &key_set, time::OffsetDateTime::now_utc().unix_timestamp()).await,
Err(AuthError::SessionRevoked),
"token A's sid was revoked",
);
verify(&token_b, &cfg, &key_set, time::OffsetDateTime::now_utc().unix_timestamp()).await.expect("token B's sid still active");
}
#[tokio::test]
async fn session_substrate_transient_maps_to_session_lookup_unavailable() {
let (signer, key_set) = SigningKey::test_pair();
let token = mint_token_with_sid(&signer, TEST_SID);
let port = Arc::new(MemorySessionRevocation::failing());
let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE).with_session_revocation(port);
let result = verify(&token, &cfg, &key_set, time::OffsetDateTime::now_utc().unix_timestamp()).await;
assert_eq!(
result,
Err(AuthError::SessionLookupUnavailable),
"failing substrate must map to SessionLookupUnavailable, not admit",
);
}
#[tokio::test]
async fn session_dyn_compatibility_via_arc_in_verify_config() {
let port: Arc<dyn SessionRevocation> = Arc::new(MemorySessionRevocation::new());
let _cfg = VerifyConfig::access_token(ISSUER, AUDIENCE).with_session_revocation(port);
}
#[tokio::test]
async fn session_with_sid_round_trips_to_claims_sid() {
let (signer, key_set) = SigningKey::test_pair();
let pinned_sid = "01H_ROUND_TRIP_SESSION_ID_X";
let token = mint_token_with_sid(&signer, pinned_sid);
let cfg = VerifyConfig::access_token(ISSUER, AUDIENCE);
let claims = verify(&token, &cfg, &key_set, time::OffsetDateTime::now_utc().unix_timestamp()).await.expect("verify");
assert_eq!(
claims.sid.as_deref(),
Some(pinned_sid),
"sid must round-trip from issue to verify unchanged",
);
}