Skip to main content

uselesskey_token/srp/
shape.rs

1//! Token shape generation primitives for test fixtures.
2//!
3//! Generates realistic-looking API keys, bearer tokens, OAuth access tokens,
4//! and scanner-safe negative token shapes from deterministic seed material.
5//!
6//! # Examples
7//!
8//! ```
9//! use uselesskey_token::srp::shape::{
10//!     NegativeToken, authorization_scheme, generate_negative_token, generate_token, TokenKind,
11//! };
12//! use uselesskey_core::Seed;
13//!
14//! let seed = Seed::new([42u8; 32]);
15//!
16//! // Generate an API key (prefixed with `uk_test_`)
17//! let api_key = generate_token("my-service", TokenKind::ApiKey, seed);
18//! assert!(api_key.starts_with("uk_test_"));
19//!
20//! // Generate a bearer token (base64url-encoded random bytes)
21//! let bearer = generate_token("my-service", TokenKind::Bearer, seed);
22//! assert_eq!(authorization_scheme(TokenKind::Bearer), "Bearer");
23//!
24//! // Generate an OAuth access token (JWT-shaped: header.payload.signature)
25//! let oauth = generate_token("my-service", TokenKind::OAuthAccessToken, seed);
26//! assert_eq!(oauth.matches('.').count(), 2);
27//!
28//! // Generate a scanner-safe negative token for validator error paths.
29//! let expired = generate_negative_token(
30//!     "my-service",
31//!     TokenKind::OAuthAccessToken,
32//!     seed,
33//!     NegativeToken::ExpiredClaims,
34//! );
35//! assert_eq!(expired.matches('.').count(), 2);
36//! ```
37
38use base64::Engine as _;
39use base64::engine::general_purpose::URL_SAFE_NO_PAD;
40use rand_chacha10::ChaCha20Rng;
41use rand_core10::{Rng, SeedableRng};
42
43use serde_json::{Map, Value, json};
44use uselesskey_core::Seed;
45
46pub use super::base62::random_base62;
47
48/// Prefix used for API-key token fixtures.
49pub const API_KEY_PREFIX: &str = "uk_test_";
50
51/// Number of random base62 characters used in API-key fixtures.
52pub const API_KEY_RANDOM_LEN: usize = 32;
53
54/// Number of raw random bytes in opaque bearer tokens.
55pub const BEARER_RANDOM_BYTES: usize = 32;
56
57/// Number of random bytes used for OAuth `jti`.
58pub const OAUTH_JTI_BYTES: usize = 16;
59
60/// Number of random bytes used for OAuth signature-like segment.
61pub const OAUTH_SIGNATURE_BYTES: usize = 32;
62
63const SCANNER_SAFE_INVALID_TOKEN_SEGMENT: &str = "not_base64url!*";
64
65const NEAR_MISS_API_KEY_PREFIX: &str = "uk_tset_";
66
67/// Token shape kind.
68pub use super::spec::TokenSpec as TokenKind;
69
70/// Negative token shape variants for downstream parser and validator tests.
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub enum NegativeToken {
73    /// Emit a JWT-like value with the wrong number of dot-separated segments.
74    MalformedJwtSegmentCount,
75    /// Replace one JWT segment with scanner-safe invalid base64url text.
76    BadBase64UrlSegment,
77    /// Encode a JWT header that is JSON, but not a header object.
78    InvalidJwtHeaderShape,
79    /// Remove `alg` from the JWT header.
80    MissingAlg,
81    /// Set the JWT header algorithm to `none`.
82    AlgNone,
83    /// Emit different `kid` values in the header and payload.
84    MismatchedKid,
85    /// Set an already-expired `exp` claim.
86    ExpiredClaims,
87    /// Set a future `nbf` claim.
88    NotYetValidClaims,
89    /// Replace the expected issuer claim.
90    BadIssuer,
91    /// Replace the expected audience claim.
92    BadAudience,
93    /// Emit a bearer-like token that is not valid base64url.
94    MalformedBearer,
95    /// Emit an API-key near miss that is close to, but not, `uk_test_`.
96    NearMissApiKey,
97}
98
99impl NegativeToken {
100    /// Stable cache/disposition name for this negative token variant.
101    pub const fn variant_name(&self) -> &'static str {
102        match self {
103            Self::MalformedJwtSegmentCount => "malformed_jwt_segment_count",
104            Self::BadBase64UrlSegment => "bad_base64url_segment",
105            Self::InvalidJwtHeaderShape => "invalid_jwt_header_shape",
106            Self::MissingAlg => "missing_alg",
107            Self::AlgNone => "alg_none",
108            Self::MismatchedKid => "mismatched_kid",
109            Self::ExpiredClaims => "expired_claims",
110            Self::NotYetValidClaims => "not_yet_valid_claims",
111            Self::BadIssuer => "bad_issuer",
112            Self::BadAudience => "bad_audience",
113            Self::MalformedBearer => "malformed_bearer",
114            Self::NearMissApiKey => "near_miss_api_key",
115        }
116    }
117}
118
119/// Generate a token value for the provided shape kind.
120pub fn generate_token(label: &str, kind: TokenKind, seed: Seed) -> String {
121    match kind {
122        TokenKind::ApiKey => generate_api_key(seed),
123        TokenKind::Bearer => generate_bearer_token(seed),
124        TokenKind::OAuthAccessToken => generate_oauth_access_token(label, seed),
125    }
126}
127
128/// Generate a scanner-safe negative token value for parser and validator tests.
129pub fn generate_negative_token(
130    label: &str,
131    kind: TokenKind,
132    seed: Seed,
133    variant: NegativeToken,
134) -> String {
135    match variant {
136        NegativeToken::MalformedJwtSegmentCount => malformed_jwt_segment_count(label, seed),
137        NegativeToken::BadBase64UrlSegment => bad_base64url_segment(label, seed),
138        NegativeToken::InvalidJwtHeaderShape => invalid_jwt_header_shape(label, seed),
139        NegativeToken::MissingAlg => missing_alg(label, seed),
140        NegativeToken::AlgNone => alg_none(label, seed),
141        NegativeToken::MismatchedKid => mismatched_kid(label, seed),
142        NegativeToken::ExpiredClaims => token_with_payload_claim(label, seed, "exp", json!(1u64)),
143        NegativeToken::NotYetValidClaims => not_yet_valid_claims(label, seed),
144        NegativeToken::BadIssuer => {
145            token_with_payload_claim(label, seed, "iss", json!("wrong-issuer"))
146        }
147        NegativeToken::BadAudience => {
148            token_with_payload_claim(label, seed, "aud", json!("wrong-audience"))
149        }
150        NegativeToken::MalformedBearer => malformed_bearer(seed),
151        NegativeToken::NearMissApiKey => near_miss_api_key(kind, seed),
152    }
153}
154
155/// Return HTTP authorization scheme for the token kind.
156pub fn authorization_scheme(kind: TokenKind) -> &'static str {
157    kind.authorization_scheme()
158}
159
160/// Generate an API-key style token fixture (`uk_test_<base62>`).
161pub fn generate_api_key(seed: Seed) -> String {
162    let mut out = String::from(API_KEY_PREFIX);
163    out.push_str(&random_base62(seed, API_KEY_RANDOM_LEN));
164    out
165}
166
167/// Generate an opaque bearer token fixture (base64url of 32 random bytes).
168pub fn generate_bearer_token(seed: Seed) -> String {
169    let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
170    let mut bytes = [0u8; BEARER_RANDOM_BYTES];
171    rng.fill_bytes(&mut bytes);
172    URL_SAFE_NO_PAD.encode(bytes)
173}
174
175/// Generate an OAuth access token fixture in JWT shape (`header.payload.signature`).
176pub fn generate_oauth_access_token(label: &str, seed: Seed) -> String {
177    let header = URL_SAFE_NO_PAD.encode(r#"{"alg":"RS256","typ":"JWT"}"#);
178    let mut rng = ChaCha20Rng::from_seed(*seed.bytes());
179
180    let mut jti = [0u8; OAUTH_JTI_BYTES];
181    rng.fill_bytes(&mut jti);
182
183    let payload = json!({
184        "iss": "uselesskey",
185        "sub": label,
186        "aud": "tests",
187        "scope": "fixture.read",
188        "jti": URL_SAFE_NO_PAD.encode(jti),
189        "exp": 2_000_000_000u64,
190    });
191    let payload_json = serde_json::to_vec(&payload).expect("payload JSON");
192    let payload_segment = URL_SAFE_NO_PAD.encode(payload_json);
193
194    let mut signature = [0u8; OAUTH_SIGNATURE_BYTES];
195    rng.fill_bytes(&mut signature);
196    let signature_segment = URL_SAFE_NO_PAD.encode(signature);
197
198    format!("{header}.{payload_segment}.{signature_segment}")
199}
200
201fn malformed_jwt_segment_count(label: &str, seed: Seed) -> String {
202    let [header, payload, _signature] = oauth_parts(label, seed);
203    format!("{header}.{payload}")
204}
205
206fn bad_base64url_segment(label: &str, seed: Seed) -> String {
207    let [header, _payload, signature] = oauth_parts(label, seed);
208    format!("{header}.{SCANNER_SAFE_INVALID_TOKEN_SEGMENT}.{signature}")
209}
210
211fn invalid_jwt_header_shape(label: &str, seed: Seed) -> String {
212    let [_header, payload, signature] = oauth_parts(label, seed);
213    let header = encode_json(&json!(["not-a-header"]));
214    format!("{header}.{payload}.{signature}")
215}
216
217fn missing_alg(label: &str, seed: Seed) -> String {
218    let [_header, payload, signature] = oauth_parts(label, seed);
219    let header = encode_json(&json!({ "typ": "JWT" }));
220    format!("{header}.{payload}.{signature}")
221}
222
223fn alg_none(label: &str, seed: Seed) -> String {
224    token_with_header_claim(label, seed, "alg", json!("none"))
225}
226
227fn mismatched_kid(label: &str, seed: Seed) -> String {
228    let [_header, payload, signature] = oauth_parts(label, seed);
229    let mut header = jwt_header();
230    header.insert("kid".to_string(), json!("unknown-kid"));
231
232    let mut payload = decode_object(&payload);
233    payload.insert("kid".to_string(), json!("expected-kid"));
234
235    format!(
236        "{}.{}.{}",
237        encode_object(&header),
238        encode_object(&payload),
239        signature
240    )
241}
242
243fn not_yet_valid_claims(label: &str, seed: Seed) -> String {
244    let [_header, payload, signature] = oauth_parts(label, seed);
245    let mut claims = decode_object(&payload);
246    claims.insert("nbf".to_string(), json!(4_000_000_000u64));
247    claims.insert("exp".to_string(), json!(4_100_000_000u64));
248
249    format!(
250        "{}.{}.{}",
251        encode_object(&jwt_header()),
252        encode_object(&claims),
253        signature
254    )
255}
256
257fn token_with_header_claim(label: &str, seed: Seed, claim: &str, value: Value) -> String {
258    let [_header, payload, signature] = oauth_parts(label, seed);
259    let mut header = jwt_header();
260    header.insert(claim.to_string(), value);
261
262    format!("{}.{}.{}", encode_object(&header), payload, signature)
263}
264
265fn token_with_payload_claim(label: &str, seed: Seed, claim: &str, value: Value) -> String {
266    let [_header, payload, signature] = oauth_parts(label, seed);
267    let mut claims = decode_object(&payload);
268    claims.insert(claim.to_string(), value);
269
270    format!(
271        "{}.{}.{}",
272        encode_object(&jwt_header()),
273        encode_object(&claims),
274        signature
275    )
276}
277
278fn malformed_bearer(seed: Seed) -> String {
279    let mut value = generate_bearer_token(seed);
280    value.replace_range(0..1, "!");
281    value
282}
283
284fn near_miss_api_key(_kind: TokenKind, seed: Seed) -> String {
285    let valid = generate_api_key(seed);
286    let suffix = valid.strip_prefix(API_KEY_PREFIX).unwrap_or(&valid);
287
288    format!("{NEAR_MISS_API_KEY_PREFIX}{suffix}")
289}
290
291fn oauth_parts(label: &str, seed: Seed) -> [String; 3] {
292    let token = generate_oauth_access_token(label, seed);
293    let mut parts = token.split('.');
294    let header = parts.next().expect("JWT header segment").to_string();
295    let payload = parts.next().expect("JWT payload segment").to_string();
296    let signature = parts.next().expect("JWT signature segment").to_string();
297    assert!(
298        parts.next().is_none(),
299        "JWT should have exactly three segments"
300    );
301
302    [header, payload, signature]
303}
304
305fn jwt_header() -> Map<String, Value> {
306    Map::from_iter([
307        ("alg".to_string(), json!("RS256")),
308        ("typ".to_string(), json!("JWT")),
309    ])
310}
311
312fn decode_object(segment: &str) -> Map<String, Value> {
313    let bytes = URL_SAFE_NO_PAD
314        .decode(segment)
315        .expect("decode generated JWT JSON segment");
316    let value: Value = serde_json::from_slice(&bytes).expect("parse generated JWT JSON segment");
317    value
318        .as_object()
319        .expect("generated JWT JSON segment should be an object")
320        .clone()
321}
322
323fn encode_object(value: &Map<String, Value>) -> String {
324    encode_json(&Value::Object(value.clone()))
325}
326
327fn encode_json(value: &Value) -> String {
328    let json = serde_json::to_vec(value).expect("serialize token JSON");
329    URL_SAFE_NO_PAD.encode(json)
330}
331
332#[cfg(test)]
333mod tests {
334    use base64::Engine as _;
335    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
336    use proptest::prelude::*;
337    use uselesskey_core::Seed;
338
339    use super::{
340        API_KEY_PREFIX, API_KEY_RANDOM_LEN, BEARER_RANDOM_BYTES, NEAR_MISS_API_KEY_PREFIX,
341        NegativeToken, SCANNER_SAFE_INVALID_TOKEN_SEGMENT, TokenKind, authorization_scheme,
342        generate_api_key, generate_bearer_token, generate_negative_token,
343        generate_oauth_access_token, generate_token, random_base62,
344    };
345
346    #[test]
347    fn api_key_shape_is_stable() {
348        let value = generate_api_key(Seed::new([7u8; 32]));
349
350        assert!(value.starts_with(API_KEY_PREFIX));
351        let suffix = value
352            .strip_prefix(API_KEY_PREFIX)
353            .expect("API key prefix should be present");
354        assert_eq!(suffix.len(), API_KEY_RANDOM_LEN);
355        assert!(suffix.chars().all(|c| c.is_ascii_alphanumeric()));
356    }
357
358    #[test]
359    fn bearer_shape_decodes_to_32_bytes() {
360        let value = generate_bearer_token(Seed::new([9u8; 32]));
361        let decoded = URL_SAFE_NO_PAD.decode(value).expect("base64url decode");
362        assert_eq!(decoded.len(), BEARER_RANDOM_BYTES);
363    }
364
365    #[test]
366    fn oauth_shape_has_three_segments_and_subject() {
367        let value = generate_oauth_access_token("issuer", Seed::new([11u8; 32]));
368        let parts: Vec<&str> = value.split('.').collect();
369        assert_eq!(parts.len(), 3);
370
371        let payload = URL_SAFE_NO_PAD
372            .decode(parts[1])
373            .expect("decode payload segment");
374        let json: serde_json::Value = serde_json::from_slice(&payload).expect("parse payload");
375        assert_eq!(json["sub"], "issuer");
376        assert_eq!(json["iss"], "uselesskey");
377    }
378
379    #[test]
380    fn authorization_scheme_matches_kind() {
381        assert_eq!(authorization_scheme(TokenKind::ApiKey), "ApiKey");
382        assert_eq!(authorization_scheme(TokenKind::Bearer), "Bearer");
383        assert_eq!(authorization_scheme(TokenKind::OAuthAccessToken), "Bearer");
384    }
385
386    #[test]
387    fn generate_token_varies_by_kind() {
388        let seed = [13u8; 32];
389
390        let api = generate_token("label", TokenKind::ApiKey, Seed::new(seed));
391        let bearer = generate_token("label", TokenKind::Bearer, Seed::new(seed));
392        let oauth = generate_token("label", TokenKind::OAuthAccessToken, Seed::new(seed));
393
394        assert_ne!(api, bearer);
395        assert_ne!(api, oauth);
396        assert_ne!(bearer, oauth);
397    }
398
399    #[test]
400    fn negative_token_variant_names_are_stable() {
401        assert_eq!(
402            NegativeToken::MalformedJwtSegmentCount.variant_name(),
403            "malformed_jwt_segment_count"
404        );
405        assert_eq!(
406            NegativeToken::BadBase64UrlSegment.variant_name(),
407            "bad_base64url_segment"
408        );
409        assert_eq!(
410            NegativeToken::InvalidJwtHeaderShape.variant_name(),
411            "invalid_jwt_header_shape"
412        );
413        assert_eq!(NegativeToken::MissingAlg.variant_name(), "missing_alg");
414        assert_eq!(NegativeToken::AlgNone.variant_name(), "alg_none");
415        assert_eq!(
416            NegativeToken::MismatchedKid.variant_name(),
417            "mismatched_kid"
418        );
419        assert_eq!(
420            NegativeToken::ExpiredClaims.variant_name(),
421            "expired_claims"
422        );
423        assert_eq!(
424            NegativeToken::NotYetValidClaims.variant_name(),
425            "not_yet_valid_claims"
426        );
427        assert_eq!(NegativeToken::BadIssuer.variant_name(), "bad_issuer");
428        assert_eq!(NegativeToken::BadAudience.variant_name(), "bad_audience");
429        assert_eq!(
430            NegativeToken::MalformedBearer.variant_name(),
431            "malformed_bearer"
432        );
433        assert_eq!(
434            NegativeToken::NearMissApiKey.variant_name(),
435            "near_miss_api_key"
436        );
437    }
438
439    #[test]
440    fn negative_api_key_near_miss_is_scanner_safe() {
441        let value = generate_negative_token(
442            "svc",
443            TokenKind::ApiKey,
444            Seed::new([19u8; 32]),
445            NegativeToken::NearMissApiKey,
446        );
447
448        assert!(value.starts_with(NEAR_MISS_API_KEY_PREFIX));
449        assert!(!value.starts_with(API_KEY_PREFIX));
450        assert_eq!(
451            value.len(),
452            NEAR_MISS_API_KEY_PREFIX.len() + API_KEY_RANDOM_LEN
453        );
454    }
455
456    #[test]
457    fn negative_malformed_bearer_is_not_base64url() {
458        let value = generate_negative_token(
459            "svc",
460            TokenKind::Bearer,
461            Seed::new([23u8; 32]),
462            NegativeToken::MalformedBearer,
463        );
464
465        assert_ne!(value, SCANNER_SAFE_INVALID_TOKEN_SEGMENT);
466        assert!(value.contains('!'));
467        assert_eq!(value.len(), 43);
468        assert!(URL_SAFE_NO_PAD.decode(value).is_err());
469    }
470
471    #[test]
472    fn negative_jwt_segment_count_keeps_two_decodable_segments() {
473        let value = generate_negative_token(
474            "svc",
475            TokenKind::OAuthAccessToken,
476            Seed::new([31u8; 32]),
477            NegativeToken::MalformedJwtSegmentCount,
478        );
479        let parts = jwt_parts(&value);
480
481        assert_eq!(parts.len(), 2);
482        assert_eq!(decode_object_segment(parts[0])["alg"], "RS256");
483        assert_eq!(decode_object_segment(parts[0])["typ"], "JWT");
484        assert_eq!(decode_object_segment(parts[1])["sub"], "svc");
485    }
486
487    #[test]
488    fn negative_bad_base64url_replaces_payload_only() {
489        let value = generate_negative_token(
490            "svc",
491            TokenKind::OAuthAccessToken,
492            Seed::new([32u8; 32]),
493            NegativeToken::BadBase64UrlSegment,
494        );
495        let parts = jwt_parts(&value);
496
497        assert_eq!(parts.len(), 3);
498        assert_eq!(decode_object_segment(parts[0])["alg"], "RS256");
499        assert_eq!(parts[1], SCANNER_SAFE_INVALID_TOKEN_SEGMENT);
500        assert!(URL_SAFE_NO_PAD.decode(parts[1]).is_err());
501        assert!(!parts[2].is_empty());
502    }
503
504    #[test]
505    fn negative_invalid_header_shape_keeps_payload_and_signature() {
506        let value = generate_negative_token(
507            "svc",
508            TokenKind::OAuthAccessToken,
509            Seed::new([33u8; 32]),
510            NegativeToken::InvalidJwtHeaderShape,
511        );
512        let parts = jwt_parts(&value);
513
514        assert_eq!(parts.len(), 3);
515        assert_eq!(
516            decode_json_segment(parts[0]),
517            serde_json::json!(["not-a-header"])
518        );
519        assert_eq!(decode_object_segment(parts[1])["sub"], "svc");
520        assert!(!parts[2].is_empty());
521    }
522
523    #[test]
524    fn negative_missing_alg_keeps_typ_and_claims() {
525        let value = generate_negative_token(
526            "svc",
527            TokenKind::OAuthAccessToken,
528            Seed::new([34u8; 32]),
529            NegativeToken::MissingAlg,
530        );
531        let parts = jwt_parts(&value);
532        let header = decode_object_segment(parts[0]);
533
534        assert_eq!(parts.len(), 3);
535        assert!(!header.contains_key("alg"));
536        assert_eq!(header["typ"], "JWT");
537        assert_eq!(decode_object_segment(parts[1])["sub"], "svc");
538    }
539
540    #[test]
541    fn negative_alg_none_changes_alg_only() {
542        let value = generate_negative_token(
543            "svc",
544            TokenKind::OAuthAccessToken,
545            Seed::new([35u8; 32]),
546            NegativeToken::AlgNone,
547        );
548        let parts = jwt_parts(&value);
549        let header = decode_object_segment(parts[0]);
550
551        assert_eq!(parts.len(), 3);
552        assert_eq!(header["alg"], "none");
553        assert_eq!(header["typ"], "JWT");
554        assert_eq!(decode_object_segment(parts[1])["sub"], "svc");
555    }
556
557    #[test]
558    fn negative_mismatched_kid_keeps_header_and_payload_context() {
559        let value = generate_negative_token(
560            "svc",
561            TokenKind::OAuthAccessToken,
562            Seed::new([36u8; 32]),
563            NegativeToken::MismatchedKid,
564        );
565        let parts = jwt_parts(&value);
566        let header = decode_object_segment(parts[0]);
567        let payload = decode_object_segment(parts[1]);
568
569        assert_eq!(parts.len(), 3);
570        assert_eq!(header["alg"], "RS256");
571        assert_eq!(header["typ"], "JWT");
572        assert_eq!(header["kid"], "unknown-kid");
573        assert_eq!(payload["sub"], "svc");
574        assert_eq!(payload["kid"], "expected-kid");
575        assert_ne!(header["kid"], payload["kid"]);
576    }
577
578    #[test]
579    fn negative_not_yet_valid_keeps_future_window_and_subject() {
580        let value = generate_negative_token(
581            "svc",
582            TokenKind::OAuthAccessToken,
583            Seed::new([37u8; 32]),
584            NegativeToken::NotYetValidClaims,
585        );
586        let parts = jwt_parts(&value);
587        let header = decode_object_segment(parts[0]);
588        let payload = decode_object_segment(parts[1]);
589
590        assert_eq!(parts.len(), 3);
591        assert_eq!(header["alg"], "RS256");
592        assert_eq!(payload["sub"], "svc");
593        assert_eq!(payload["nbf"], 4_000_000_000u64);
594        assert_eq!(payload["exp"], 4_100_000_000u64);
595    }
596
597    fn jwt_parts(value: &str) -> Vec<&str> {
598        value.split('.').collect()
599    }
600
601    fn decode_object_segment(segment: &str) -> serde_json::Map<String, serde_json::Value> {
602        decode_json_segment(segment)
603            .as_object()
604            .expect("JWT segment should decode to an object")
605            .clone()
606    }
607
608    fn decode_json_segment(segment: &str) -> serde_json::Value {
609        let bytes = URL_SAFE_NO_PAD.decode(segment).expect("decode JWT segment");
610        serde_json::from_slice(&bytes).expect("parse JWT segment JSON")
611    }
612
613    #[test]
614    fn random_base62_length_and_charset() {
615        let value = random_base62(Seed::new([17u8; 32]), 64);
616        assert_eq!(value.len(), 64);
617        assert!(value.chars().all(|c| c.is_ascii_alphanumeric()));
618    }
619
620    proptest! {
621        #[test]
622        fn api_key_same_seed_stable(seed in any::<[u8; 32]>()) {
623            let a = generate_api_key(Seed::new(seed));
624            let b = generate_api_key(Seed::new(seed));
625            prop_assert_eq!(a, b);
626        }
627
628        #[test]
629        fn bearer_token_always_43_chars(seed in any::<[u8; 32]>()) {
630            let token = generate_bearer_token(Seed::new(seed));
631            prop_assert_eq!(token.len(), 43);
632        }
633
634        #[test]
635        fn oauth_has_three_segments(seed in any::<[u8; 32]>(), label in "[a-z0-9_-]{1,16}") {
636            let token = generate_oauth_access_token(&label, Seed::new(seed));
637            prop_assert_eq!(token.matches('.').count(), 2);
638        }
639    }
640}