Skip to main content

uselesskey_token/
token.rs

1use std::fmt;
2use std::sync::Arc;
3
4use uselesskey_core::Factory;
5use uselesskey_core_token::generate_token;
6
7use crate::TokenSpec;
8
9/// Cache domain for token fixtures.
10///
11/// Keep this stable: changing it changes deterministic outputs.
12pub const DOMAIN_TOKEN_FIXTURE: &str = "uselesskey:token:fixture";
13
14/// A token fixture with a generated value.
15///
16/// Created via [`TokenFactoryExt::token()`]. Provides access to
17/// the generated token value and an HTTP `Authorization` header.
18///
19/// # Examples
20///
21/// ```
22/// # use uselesskey_core::{Factory, Seed};
23/// # use uselesskey_token::{TokenFactoryExt, TokenSpec};
24/// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
25/// let tok = fx.token("api-key", TokenSpec::api_key());
26/// assert!(tok.value().starts_with("uk_test_"));
27/// ```
28#[derive(Clone)]
29pub struct TokenFixture {
30    factory: Factory,
31    label: String,
32    spec: TokenSpec,
33    inner: Arc<Inner>,
34}
35
36struct Inner {
37    value: String,
38}
39
40impl fmt::Debug for TokenFixture {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        f.debug_struct("TokenFixture")
43            .field("label", &self.label)
44            .field("spec", &self.spec)
45            .finish_non_exhaustive()
46    }
47}
48
49/// Extension trait to hang token helpers off the core [`Factory`].
50pub trait TokenFactoryExt {
51    /// Generate (or retrieve from cache) a token fixture.
52    ///
53    /// The `label` identifies this token within your test suite.
54    /// In deterministic mode, `seed + label + spec` always produces the same token.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// # use uselesskey_core::{Factory, Seed};
60    /// # use uselesskey_token::{TokenFactoryExt, TokenSpec};
61    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
62    /// let tok = fx.token("billing", TokenSpec::bearer());
63    /// assert!(!tok.value().is_empty());
64    /// ```
65    fn token(&self, label: impl AsRef<str>, spec: TokenSpec) -> TokenFixture;
66
67    /// Generate a token fixture with an explicit variant.
68    ///
69    /// Different variants for the same `(label, spec)` produce different tokens.
70    ///
71    /// # Examples
72    ///
73    /// ```
74    /// # use uselesskey_core::{Factory, Seed};
75    /// # use uselesskey_token::{TokenFactoryExt, TokenSpec};
76    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
77    /// let good = fx.token("svc", TokenSpec::api_key());
78    /// let alt = fx.token_with_variant("svc", TokenSpec::api_key(), "alt");
79    /// assert_ne!(good.value(), alt.value());
80    /// ```
81    fn token_with_variant(
82        &self,
83        label: impl AsRef<str>,
84        spec: TokenSpec,
85        variant: impl AsRef<str>,
86    ) -> TokenFixture;
87}
88
89impl TokenFactoryExt for Factory {
90    fn token(&self, label: impl AsRef<str>, spec: TokenSpec) -> TokenFixture {
91        TokenFixture::new(self.clone(), label.as_ref(), spec)
92    }
93
94    fn token_with_variant(
95        &self,
96        label: impl AsRef<str>,
97        spec: TokenSpec,
98        variant: impl AsRef<str>,
99    ) -> TokenFixture {
100        let label = label.as_ref();
101        let variant = variant.as_ref();
102        let factory = self.clone();
103        let inner = load_inner(&factory, label, spec, variant);
104        TokenFixture {
105            factory,
106            label: label.to_string(),
107            spec,
108            inner,
109        }
110    }
111}
112
113impl TokenFixture {
114    fn new(factory: Factory, label: &str, spec: TokenSpec) -> Self {
115        let inner = load_inner(&factory, label, spec, "good");
116        Self {
117            factory,
118            label: label.to_string(),
119            spec,
120            inner,
121        }
122    }
123
124    #[allow(dead_code)]
125    fn load_variant(&self, variant: &str) -> Arc<Inner> {
126        load_inner(&self.factory, &self.label, self.spec, variant)
127    }
128
129    /// Access the token value.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// # use uselesskey_core::{Factory, Seed};
135    /// # use uselesskey_token::{TokenFactoryExt, TokenSpec};
136    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
137    /// let tok = fx.token("svc", TokenSpec::api_key());
138    /// let val = tok.value();
139    /// assert!(val.starts_with("uk_test_"));
140    /// ```
141    pub fn value(&self) -> &str {
142        &self.inner.value
143    }
144
145    /// Returns an HTTP `Authorization` header value for this token.
146    ///
147    /// - API keys use `ApiKey <token>`
148    /// - Bearer and OAuth access tokens use `Bearer <token>`
149    ///
150    /// # Examples
151    ///
152    /// ```
153    /// # use uselesskey_core::{Factory, Seed};
154    /// # use uselesskey_token::{TokenFactoryExt, TokenSpec};
155    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
156    ///
157    /// let bearer = fx.token("svc", TokenSpec::bearer());
158    /// assert!(bearer.authorization_header().starts_with("Bearer "));
159    ///
160    /// let api = fx.token("svc", TokenSpec::api_key());
161    /// assert!(api.authorization_header().starts_with("ApiKey "));
162    /// ```
163    pub fn authorization_header(&self) -> String {
164        let scheme = self.spec.authorization_scheme();
165        format!("{scheme} {}", self.value())
166    }
167}
168
169fn load_inner(factory: &Factory, label: &str, spec: TokenSpec, variant: &str) -> Arc<Inner> {
170    let spec_bytes = spec.stable_bytes();
171
172    factory.get_or_init(DOMAIN_TOKEN_FIXTURE, label, &spec_bytes, variant, |rng| {
173        let value = generate_token(label, spec, rng);
174        Inner { value }
175    })
176}
177
178#[cfg(test)]
179mod tests {
180    use base64::Engine as _;
181    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
182
183    use super::*;
184    use uselesskey_core::Seed;
185
186    #[test]
187    fn deterministic_token_is_stable() {
188        let fx = Factory::deterministic(Seed::from_env_value("token-det").unwrap());
189        let t1 = fx.token("svc", TokenSpec::api_key());
190        let t2 = fx.token("svc", TokenSpec::api_key());
191        assert_eq!(t1.value(), t2.value());
192    }
193
194    #[test]
195    fn random_mode_still_caches_per_identity() {
196        let fx = Factory::random();
197        let t1 = fx.token("svc", TokenSpec::bearer());
198        let t2 = fx.token("svc", TokenSpec::bearer());
199        assert_eq!(t1.value(), t2.value());
200    }
201
202    #[test]
203    fn different_labels_produce_different_tokens() {
204        let fx = Factory::deterministic(Seed::from_env_value("token-label").unwrap());
205        let a = fx.token("a", TokenSpec::bearer());
206        let b = fx.token("b", TokenSpec::bearer());
207        assert_ne!(a.value(), b.value());
208    }
209
210    #[test]
211    fn api_key_shape_is_realistic() {
212        let fx = Factory::random();
213        let token = fx.token("svc", TokenSpec::api_key());
214
215        assert!(token.value().starts_with("uk_test_"));
216        let suffix = &token.value()["uk_test_".len()..];
217        assert_eq!(suffix.len(), 32);
218        assert!(suffix.chars().all(|c| c.is_ascii_alphanumeric()));
219    }
220
221    #[test]
222    fn bearer_header_uses_bearer_scheme() {
223        let fx = Factory::random();
224        let token = fx.token("svc", TokenSpec::bearer());
225        let header = token.authorization_header();
226        assert!(header.starts_with("Bearer "));
227        assert!(header.ends_with(token.value()));
228    }
229
230    #[test]
231    fn oauth_token_has_three_segments_and_json_header() {
232        let fx = Factory::deterministic(Seed::from_env_value("token-oauth").unwrap());
233        let token = fx.token("issuer", TokenSpec::oauth_access_token());
234
235        let parts: Vec<&str> = token.value().split('.').collect();
236        assert_eq!(parts.len(), 3);
237
238        let header_bytes = URL_SAFE_NO_PAD
239            .decode(parts[0])
240            .expect("decode JWT header segment");
241        let payload_bytes = URL_SAFE_NO_PAD
242            .decode(parts[1])
243            .expect("decode JWT payload segment");
244
245        let header: serde_json::Value = serde_json::from_slice(&header_bytes).expect("header json");
246        let payload: serde_json::Value =
247            serde_json::from_slice(&payload_bytes).expect("payload json");
248
249        assert_eq!(header["alg"], "RS256");
250        assert_eq!(header["typ"], "JWT");
251        assert_eq!(payload["sub"], "issuer");
252        assert_eq!(payload["iss"], "uselesskey");
253    }
254
255    #[test]
256    fn different_variants_produce_different_tokens() {
257        let fx = Factory::deterministic(Seed::from_env_value("token-variant").unwrap());
258        let token = fx.token("svc", TokenSpec::bearer());
259        let other = token.load_variant("other");
260
261        assert_ne!(token.value(), other.value.as_str());
262    }
263
264    #[test]
265    fn token_with_variant_uses_custom_variant() {
266        let fx = Factory::deterministic(Seed::from_env_value("token-variant2").unwrap());
267        let good = fx.token("svc", TokenSpec::api_key());
268        let custom = fx.token_with_variant("svc", TokenSpec::api_key(), "custom");
269
270        assert_ne!(good.value(), custom.value());
271    }
272
273    #[test]
274    fn debug_does_not_include_token_value() {
275        let fx = Factory::random();
276        let token = fx.token("debug-label", TokenSpec::api_key());
277        let dbg = format!("{token:?}");
278        assert!(dbg.contains("TokenFixture"));
279        assert!(dbg.contains("debug-label"));
280        assert!(!dbg.contains(token.value()));
281    }
282
283    #[test]
284    fn random_base62_uses_full_alphabet() {
285        let fx = Factory::deterministic(Seed::from_env_value("base62-test").unwrap());
286        let t = fx.token("alphabet-test", TokenSpec::api_key());
287        let value = t.value();
288        // API key format: "uk_test_{32 random base62 chars}".
289        // Strip the prefix to inspect only the random suffix.
290        let suffix = value.strip_prefix("uk_test_").expect("API key prefix");
291        // With / instead of %, only A-E would appear (byte[0] / 62 yields 0..=4).
292        // With %, the full base62 alphabet is used. A 32-char random suffix must
293        // contain characters beyond the first five uppercase letters.
294        assert!(
295            suffix
296                .chars()
297                .any(|c| c.is_ascii_lowercase() || c.is_ascii_digit()),
298            "random suffix should use full base62 alphabet, got: {suffix}"
299        );
300    }
301}