uselesskey_token/
token.rs1use std::fmt;
2use std::sync::Arc;
3
4use uselesskey_core::Factory;
5use uselesskey_core_token::generate_token;
6
7use crate::TokenSpec;
8
9pub const DOMAIN_TOKEN_FIXTURE: &str = "uselesskey:token:fixture";
13
14#[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
49pub trait TokenFactoryExt {
51 fn token(&self, label: impl AsRef<str>, spec: TokenSpec) -> TokenFixture;
66
67 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 pub fn spec(&self) -> TokenSpec {
141 self.spec
142 }
143
144 pub fn label(&self) -> &str {
156 &self.label
157 }
158
159 pub fn value(&self) -> &str {
172 &self.inner.value
173 }
174
175 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 let suffix = value.strip_prefix("uk_test_").expect("API key prefix");
321 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}