1
use crate::common::{TEST_PRIVATE_KEY_B64, TEST_PUBLIC_KEY_B64};
2
use uuid::Uuid;
3
use web::token::{
4
    TokenClaims, TokenDetails, TokenType, generate_jwt_token, generate_jwt_token_with_uuid,
5
    verify_jwt_token,
6
};
7

            
8
#[test]
9
1
fn test_token_details_creation() {
10
1
    let user_id = Uuid::new_v4();
11
1
    let token_uuid = Uuid::new_v4();
12

            
13
1
    let token_details = TokenDetails {
14
1
        generation: 0,
15
1
        token: Some("test_token".to_string()),
16
1
        token_uuid,
17
1
        user_id,
18
1
        token_type: Some(TokenType::Access),
19
1
        expires_in: Some(1234567890),
20
1
    };
21

            
22
1
    assert_eq!(token_details.user_id, user_id);
23
1
    assert_eq!(token_details.token_uuid, token_uuid);
24
1
    assert_eq!(token_details.token, Some("test_token".to_string()));
25
1
    assert_eq!(token_details.expires_in, Some(1234567890));
26
1
}
27

            
28
#[test]
29
1
fn test_token_claims_creation() {
30
1
    let user_id = Uuid::new_v4();
31
1
    let token_uuid = Uuid::new_v4();
32

            
33
1
    let claims = TokenClaims {
34
1
        generation: 0,
35
1
        sub: user_id.to_string(),
36
1
        token_uuid: token_uuid.to_string(),
37
1
        token_type: Some(TokenType::Refresh),
38
1
        exp: 1234567890,
39
1
        iat: 1234567000,
40
1
        nbf: 1234567000,
41
1
    };
42

            
43
1
    assert_eq!(claims.sub, user_id.to_string());
44
1
    assert_eq!(claims.token_uuid, token_uuid.to_string());
45
1
    assert_eq!(claims.exp, 1234567890);
46
1
    assert_eq!(claims.iat, 1234567000);
47
1
    assert_eq!(claims.nbf, 1234567000);
48
1
}
49

            
50
#[test]
51
1
fn test_generate_jwt_token_round_trip() {
52
1
    let user_id = Uuid::new_v4();
53
1
    let details = generate_jwt_token(user_id, 15, TEST_PRIVATE_KEY_B64, TokenType::Access, 7)
54
1
        .expect("encode");
55
1
    let token = details.token.as_deref().expect("token");
56

            
57
1
    let verified = verify_jwt_token(TEST_PUBLIC_KEY_B64, token).expect("verify");
58

            
59
1
    assert_eq!(verified.user_id, user_id);
60
1
    assert_eq!(
61
        verified.generation, 7,
62
        "the generation must ride in the claims"
63
    );
64
1
    assert_eq!(verified.token_uuid, details.token_uuid);
65
1
    assert_eq!(
66
        verified.token_type,
67
        Some(TokenType::Access),
68
        "token_type claim must survive the sign/verify round trip"
69
    );
70
    // Dropped on decode, this silently disabled every caller that bounds work
71
    // by a token's remaining life — the refresh grace window wrote no marker
72
    // at all, and nothing failed: the feature was simply inert.
73
1
    assert_eq!(
74
        verified.expires_in, details.expires_in,
75
        "exp must survive the round trip, not come back as None"
76
    );
77
1
    let remaining = verified.expires_in.expect("exp survives") - chrono::Utc::now().timestamp();
78
1
    assert!(
79
1
        (14 * 60..=15 * 60).contains(&remaining),
80
        "a 15-minute token must report about 15 minutes left, got {remaining}s"
81
    );
82
1
}
83

            
84
#[test]
85
1
fn test_generate_jwt_token_with_uuid_preserves_uuid() {
86
1
    let user_id = Uuid::new_v4();
87
1
    let fixed_uuid = Uuid::new_v4();
88

            
89
1
    let details = generate_jwt_token_with_uuid(
90
1
        user_id,
91
1
        fixed_uuid,
92
        15,
93
1
        TEST_PRIVATE_KEY_B64,
94
1
        TokenType::Refresh,
95
        0,
96
    )
97
1
    .expect("encode");
98

            
99
1
    assert_eq!(details.token_uuid, fixed_uuid);
100

            
101
1
    let verified =
102
1
        verify_jwt_token(TEST_PUBLIC_KEY_B64, details.token.as_deref().unwrap()).expect("verify");
103
1
    assert_eq!(verified.token_uuid, fixed_uuid);
104
1
    assert_eq!(verified.user_id, user_id);
105
1
}
106

            
107
#[test]
108
1
fn test_remint_with_same_uuid_extends_expiry() {
109
1
    let user_id = Uuid::new_v4();
110
1
    let shared_uuid = Uuid::new_v4();
111

            
112
1
    let first = generate_jwt_token_with_uuid(
113
1
        user_id,
114
1
        shared_uuid,
115
        15,
116
1
        TEST_PRIVATE_KEY_B64,
117
1
        TokenType::Refresh,
118
        0,
119
    )
120
1
    .expect("first");
121

            
122
1
    std::thread::sleep(std::time::Duration::from_secs(1));
123

            
124
1
    let second = generate_jwt_token_with_uuid(
125
1
        user_id,
126
1
        shared_uuid,
127
        60,
128
1
        TEST_PRIVATE_KEY_B64,
129
1
        TokenType::Refresh,
130
        0,
131
    )
132
1
    .expect("second");
133

            
134
1
    assert_eq!(first.token_uuid, second.token_uuid);
135
1
    assert!(
136
1
        second.expires_in.unwrap() > first.expires_in.unwrap(),
137
        "re-mint must push exp forward: first={:?}, second={:?}",
138
        first.expires_in,
139
        second.expires_in,
140
    );
141
1
    assert_ne!(
142
        first.token, second.token,
143
        "JWT string must differ (different iat/exp claims)"
144
    );
145

            
146
1
    let verified_first =
147
1
        verify_jwt_token(TEST_PUBLIC_KEY_B64, first.token.as_deref().unwrap()).expect("v1");
148
1
    let verified_second =
149
1
        verify_jwt_token(TEST_PUBLIC_KEY_B64, second.token.as_deref().unwrap()).expect("v2");
150
1
    assert_eq!(verified_first.token_uuid, verified_second.token_uuid);
151
1
}
152

            
153
#[test]
154
1
fn test_generate_jwt_token_unique_uuid_per_call() {
155
1
    let user_id = Uuid::new_v4();
156

            
157
1
    let a = generate_jwt_token(user_id, 15, TEST_PRIVATE_KEY_B64, TokenType::Access, 0).expect("a");
158
1
    let b = generate_jwt_token(user_id, 15, TEST_PRIVATE_KEY_B64, TokenType::Access, 0).expect("b");
159

            
160
1
    assert_ne!(
161
        a.token_uuid, b.token_uuid,
162
        "generate_jwt_token must mint fresh uuid each call"
163
    );
164
1
}
165

            
166
#[test]
167
1
fn test_unverified_user_id_reads_sub_without_signature() {
168
1
    let user_id = Uuid::new_v4();
169
1
    let details = generate_jwt_token(user_id, 15, TEST_PRIVATE_KEY_B64, TokenType::Access, 0)
170
1
        .expect("encode");
171
1
    let token = details.token.as_deref().expect("token");
172

            
173
    // The routing hint reads the sub from an unverified token; it must equal the
174
    // signer's user id (and the token still verifies normally with the key).
175
1
    let routed = web::token::unverified_user_id(token).expect("read sub");
176
1
    assert_eq!(routed, user_id);
177
1
}
178

            
179
/// A token minted before the claim existed must FAIL to decode rather than
180
/// default to generation 0 — `#[serde(default)]` here would let every pre-A2
181
/// token survive to its own `exp`, which is exactly the revocation hole the
182
/// generation exists to close.
183
#[test]
184
1
fn a_token_without_a_generation_claim_is_rejected() {
185
    #[derive(serde::Serialize)]
186
    struct LegacyClaims {
187
        sub: String,
188
        token_uuid: String,
189
        token_type: &'static str,
190
        exp: i64,
191
        iat: i64,
192
        nbf: i64,
193
    }
194

            
195
1
    let now = chrono::Utc::now();
196
1
    let key = <base64::engine::general_purpose::GeneralPurpose as base64::Engine>::decode(
197
1
        &base64::engine::general_purpose::STANDARD,
198
        TEST_PRIVATE_KEY_B64,
199
    )
200
1
    .expect("b64");
201
1
    let legacy = jsonwebtoken::encode(
202
1
        &jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256),
203
1
        &LegacyClaims {
204
1
            sub: Uuid::new_v4().to_string(),
205
1
            token_uuid: Uuid::new_v4().to_string(),
206
1
            token_type: "access",
207
1
            exp: (now + chrono::Duration::minutes(15)).timestamp(),
208
1
            iat: now.timestamp(),
209
1
            nbf: now.timestamp(),
210
1
        },
211
1
        &jsonwebtoken::EncodingKey::from_rsa_pem(&key).expect("key"),
212
    )
213
1
    .expect("encode legacy");
214

            
215
1
    assert!(
216
1
        verify_jwt_token(TEST_PUBLIC_KEY_B64, &legacy).is_err(),
217
        "a claim-less token must not decode"
218
    );
219
1
}