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    /// Returns the spec used to create this token.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// # use uselesskey_core::Factory;
135    /// # use uselesskey_token::{TokenFactoryExt, TokenSpec};
136    /// let fx = Factory::random();
137    /// let tok = fx.token("svc", TokenSpec::api_key());
138    /// assert_eq!(tok.spec(), TokenSpec::api_key());
139    /// ```
140    pub fn spec(&self) -> TokenSpec {
141        self.spec
142    }
143
144    /// Returns the label used to create this token.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// # use uselesskey_core::Factory;
150    /// # use uselesskey_token::{TokenFactoryExt, TokenSpec};
151    /// let fx = Factory::random();
152    /// let tok = fx.token("my-svc", TokenSpec::api_key());
153    /// assert_eq!(tok.label(), "my-svc");
154    /// ```
155    pub fn label(&self) -> &str {
156        &self.label
157    }
158
159    /// Access the token value.
160    ///
161    /// # Examples
162    ///
163    /// ```
164    /// # use uselesskey_core::{Factory, Seed};
165    /// # use uselesskey_token::{TokenFactoryExt, TokenSpec};
166    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
167    /// let tok = fx.token("svc", TokenSpec::api_key());
168    /// let val = tok.value();
169    /// assert!(val.starts_with("uk_test_"));
170    /// ```
171    pub fn value(&self) -> &str {
172        &self.inner.value
173    }
174
175    /// Returns an HTTP `Authorization` header value for this token.
176    ///
177    /// - API keys use `ApiKey <token>`
178    /// - Bearer and OAuth access tokens use `Bearer <token>`
179    ///
180    /// # Examples
181    ///
182    /// ```
183    /// # use uselesskey_core::{Factory, Seed};
184    /// # use uselesskey_token::{TokenFactoryExt, TokenSpec};
185    /// let fx = Factory::deterministic(Seed::from_env_value("test-seed").unwrap());
186    ///
187    /// let bearer = fx.token("svc", TokenSpec::bearer());
188    /// assert!(bearer.authorization_header().starts_with("Bearer "));
189    ///
190    /// let api = fx.token("svc", TokenSpec::api_key());
191    /// assert!(api.authorization_header().starts_with("ApiKey "));
192    /// ```
193    pub fn authorization_header(&self) -> String {
194        let scheme = self.spec.authorization_scheme();
195        format!("{scheme} {}", self.value())
196    }
197}
198
199fn load_inner(factory: &Factory, label: &str, spec: TokenSpec, variant: &str) -> Arc<Inner> {
200    let spec_bytes = spec.stable_bytes();
201
202    factory.get_or_init(DOMAIN_TOKEN_FIXTURE, label, &spec_bytes, variant, |seed| {
203        let value = generate_token(label, spec, seed);
204        Inner { value }
205    })
206}
207
208#[cfg(test)]
209mod tests {
210    use base64::Engine as _;
211    use base64::engine::general_purpose::URL_SAFE_NO_PAD;
212
213    use super::*;
214    use uselesskey_core::Seed;
215
216    #[test]
217    fn deterministic_token_is_stable() {
218        let fx = Factory::deterministic(Seed::from_env_value("token-det").unwrap());
219        let t1 = fx.token("svc", TokenSpec::api_key());
220        let t2 = fx.token("svc", TokenSpec::api_key());
221        assert_eq!(t1.value(), t2.value());
222    }
223
224    #[test]
225    fn random_mode_still_caches_per_identity() {
226        let fx = Factory::random();
227        let t1 = fx.token("svc", TokenSpec::bearer());
228        let t2 = fx.token("svc", TokenSpec::bearer());
229        assert_eq!(t1.value(), t2.value());
230    }
231
232    #[test]
233    fn different_labels_produce_different_tokens() {
234        let fx = Factory::deterministic(Seed::from_env_value("token-label").unwrap());
235        let a = fx.token("a", TokenSpec::bearer());
236        let b = fx.token("b", TokenSpec::bearer());
237        assert_ne!(a.value(), b.value());
238    }
239
240    #[test]
241    fn api_key_shape_is_realistic() {
242        let fx = Factory::random();
243        let token = fx.token("svc", TokenSpec::api_key());
244
245        assert!(token.value().starts_with("uk_test_"));
246        let suffix = &token.value()["uk_test_".len()..];
247        assert_eq!(suffix.len(), 32);
248        assert!(suffix.chars().all(|c| c.is_ascii_alphanumeric()));
249    }
250
251    #[test]
252    fn bearer_header_uses_bearer_scheme() {
253        let fx = Factory::random();
254        let token = fx.token("svc", TokenSpec::bearer());
255        let header = token.authorization_header();
256        assert!(header.starts_with("Bearer "));
257        assert!(header.ends_with(token.value()));
258    }
259
260    #[test]
261    fn oauth_token_has_three_segments_and_json_header() {
262        let fx = Factory::deterministic(Seed::from_env_value("token-oauth").unwrap());
263        let token = fx.token("issuer", TokenSpec::oauth_access_token());
264
265        let parts: Vec<&str> = token.value().split('.').collect();
266        assert_eq!(parts.len(), 3);
267
268        let header_bytes = URL_SAFE_NO_PAD
269            .decode(parts[0])
270            .expect("decode JWT header segment");
271        let payload_bytes = URL_SAFE_NO_PAD
272            .decode(parts[1])
273            .expect("decode JWT payload segment");
274
275        let header: serde_json::Value = serde_json::from_slice(&header_bytes).expect("header json");
276        let payload: serde_json::Value =
277            serde_json::from_slice(&payload_bytes).expect("payload json");
278
279        assert_eq!(header["alg"], "RS256");
280        assert_eq!(header["typ"], "JWT");
281        assert_eq!(payload["sub"], "issuer");
282        assert_eq!(payload["iss"], "uselesskey");
283    }
284
285    #[test]
286    fn different_variants_produce_different_tokens() {
287        let fx = Factory::deterministic(Seed::from_env_value("token-variant").unwrap());
288        let token = fx.token("svc", TokenSpec::bearer());
289        let other = token.load_variant("other");
290
291        assert_ne!(token.value(), other.value.as_str());
292    }
293
294    #[test]
295    fn token_with_variant_uses_custom_variant() {
296        let fx = Factory::deterministic(Seed::from_env_value("token-variant2").unwrap());
297        let good = fx.token("svc", TokenSpec::api_key());
298        let custom = fx.token_with_variant("svc", TokenSpec::api_key(), "custom");
299
300        assert_ne!(good.value(), custom.value());
301    }
302
303    #[test]
304    fn debug_does_not_include_token_value() {
305        let fx = Factory::random();
306        let token = fx.token("debug-label", TokenSpec::api_key());
307        let dbg = format!("{token:?}");
308        assert!(dbg.contains("TokenFixture"));
309        assert!(dbg.contains("debug-label"));
310        assert!(!dbg.contains(token.value()));
311    }
312
313    #[test]
314    fn random_base62_uses_full_alphabet() {
315        let fx = Factory::deterministic(Seed::from_env_value("base62-test").unwrap());
316        let t = fx.token("alphabet-test", TokenSpec::api_key());
317        let value = t.value();
318        // API key format: "uk_test_{32 random base62 chars}".
319        // Strip the prefix to inspect only the random suffix.
320        let suffix = value.strip_prefix("uk_test_").expect("API key prefix");
321        // With / instead of %, only A-E would appear (byte[0] / 62 yields 0..=4).
322        // With %, the full base62 alphabet is used. A 32-char random suffix must
323        // contain characters beyond the first five uppercase letters.
324        assert!(
325            suffix
326                .chars()
327                .any(|c| c.is_ascii_lowercase() || c.is_ascii_digit()),
328            "random suffix should use full base62 alphabet, got: {suffix}"
329        );
330    }
331}