Lines
100 %
Functions
Branches
use crate::common::{TEST_PRIVATE_KEY_B64, TEST_PUBLIC_KEY_B64};
use uuid::Uuid;
use web::token::{
TokenClaims, TokenDetails, TokenType, generate_jwt_token, generate_jwt_token_with_uuid,
verify_jwt_token,
};
#[test]
fn test_token_details_creation() {
let user_id = Uuid::new_v4();
let token_uuid = Uuid::new_v4();
let token_details = TokenDetails {
generation: 0,
token: Some("test_token".to_string()),
token_uuid,
user_id,
token_type: Some(TokenType::Access),
expires_in: Some(1234567890),
assert_eq!(token_details.user_id, user_id);
assert_eq!(token_details.token_uuid, token_uuid);
assert_eq!(token_details.token, Some("test_token".to_string()));
assert_eq!(token_details.expires_in, Some(1234567890));
}
fn test_token_claims_creation() {
let claims = TokenClaims {
sub: user_id.to_string(),
token_uuid: token_uuid.to_string(),
token_type: Some(TokenType::Refresh),
exp: 1234567890,
iat: 1234567000,
nbf: 1234567000,
assert_eq!(claims.sub, user_id.to_string());
assert_eq!(claims.token_uuid, token_uuid.to_string());
assert_eq!(claims.exp, 1234567890);
assert_eq!(claims.iat, 1234567000);
assert_eq!(claims.nbf, 1234567000);
fn test_generate_jwt_token_round_trip() {
let details = generate_jwt_token(user_id, 15, TEST_PRIVATE_KEY_B64, TokenType::Access, 7)
.expect("encode");
let token = details.token.as_deref().expect("token");
let verified = verify_jwt_token(TEST_PUBLIC_KEY_B64, token).expect("verify");
assert_eq!(verified.user_id, user_id);
assert_eq!(
verified.generation, 7,
"the generation must ride in the claims"
);
assert_eq!(verified.token_uuid, details.token_uuid);
verified.token_type,
Some(TokenType::Access),
"token_type claim must survive the sign/verify round trip"
// Dropped on decode, this silently disabled every caller that bounds work
// by a token's remaining life — the refresh grace window wrote no marker
// at all, and nothing failed: the feature was simply inert.
verified.expires_in, details.expires_in,
"exp must survive the round trip, not come back as None"
let remaining = verified.expires_in.expect("exp survives") - chrono::Utc::now().timestamp();
assert!(
(14 * 60..=15 * 60).contains(&remaining),
"a 15-minute token must report about 15 minutes left, got {remaining}s"
fn test_generate_jwt_token_with_uuid_preserves_uuid() {
let fixed_uuid = Uuid::new_v4();
let details = generate_jwt_token_with_uuid(
fixed_uuid,
15,
TEST_PRIVATE_KEY_B64,
TokenType::Refresh,
0,
)
assert_eq!(details.token_uuid, fixed_uuid);
let verified =
verify_jwt_token(TEST_PUBLIC_KEY_B64, details.token.as_deref().unwrap()).expect("verify");
assert_eq!(verified.token_uuid, fixed_uuid);
fn test_remint_with_same_uuid_extends_expiry() {
let shared_uuid = Uuid::new_v4();
let first = generate_jwt_token_with_uuid(
shared_uuid,
.expect("first");
std::thread::sleep(std::time::Duration::from_secs(1));
let second = generate_jwt_token_with_uuid(
60,
.expect("second");
assert_eq!(first.token_uuid, second.token_uuid);
second.expires_in.unwrap() > first.expires_in.unwrap(),
"re-mint must push exp forward: first={:?}, second={:?}",
first.expires_in,
second.expires_in,
assert_ne!(
first.token, second.token,
"JWT string must differ (different iat/exp claims)"
let verified_first =
verify_jwt_token(TEST_PUBLIC_KEY_B64, first.token.as_deref().unwrap()).expect("v1");
let verified_second =
verify_jwt_token(TEST_PUBLIC_KEY_B64, second.token.as_deref().unwrap()).expect("v2");
assert_eq!(verified_first.token_uuid, verified_second.token_uuid);
fn test_generate_jwt_token_unique_uuid_per_call() {
let a = generate_jwt_token(user_id, 15, TEST_PRIVATE_KEY_B64, TokenType::Access, 0).expect("a");
let b = generate_jwt_token(user_id, 15, TEST_PRIVATE_KEY_B64, TokenType::Access, 0).expect("b");
a.token_uuid, b.token_uuid,
"generate_jwt_token must mint fresh uuid each call"
fn test_unverified_user_id_reads_sub_without_signature() {
let details = generate_jwt_token(user_id, 15, TEST_PRIVATE_KEY_B64, TokenType::Access, 0)
// The routing hint reads the sub from an unverified token; it must equal the
// signer's user id (and the token still verifies normally with the key).
let routed = web::token::unverified_user_id(token).expect("read sub");
assert_eq!(routed, user_id);
/// A token minted before the claim existed must FAIL to decode rather than
/// default to generation 0 — `#[serde(default)]` here would let every pre-A2
/// token survive to its own `exp`, which is exactly the revocation hole the
/// generation exists to close.
fn a_token_without_a_generation_claim_is_rejected() {
#[derive(serde::Serialize)]
struct LegacyClaims {
sub: String,
token_uuid: String,
token_type: &'static str,
exp: i64,
iat: i64,
nbf: i64,
let now = chrono::Utc::now();
let key = <base64::engine::general_purpose::GeneralPurpose as base64::Engine>::decode(
&base64::engine::general_purpose::STANDARD,
.expect("b64");
let legacy = jsonwebtoken::encode(
&jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256),
&LegacyClaims {
sub: Uuid::new_v4().to_string(),
token_uuid: Uuid::new_v4().to_string(),
token_type: "access",
exp: (now + chrono::Duration::minutes(15)).timestamp(),
iat: now.timestamp(),
nbf: now.timestamp(),
},
&jsonwebtoken::EncodingKey::from_rsa_pem(&key).expect("key"),
.expect("encode legacy");
verify_jwt_token(TEST_PUBLIC_KEY_B64, &legacy).is_err(),
"a claim-less token must not decode"