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 value(&self) -> &str {
142 &self.inner.value
143 }
144
145 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, |seed| {
173 let value = generate_token(label, spec, seed);
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 let suffix = value.strip_prefix("uk_test_").expect("API key prefix");
291 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}