use std::fmt;
use std::sync::Arc;
use uselesskey_core::Factory;
use crate::TokenSpec;
use crate::srp::shape::{NegativeToken, generate_negative_token, generate_token};
pub const DOMAIN_TOKEN_FIXTURE: &str = "uselesskey:token:fixture";
#[derive(Clone)]
pub struct TokenFixture {
factory: Factory,
label: String,
spec: TokenSpec,
inner: Arc<Inner>,
}
struct Inner {
value: String,
}
impl fmt::Debug for TokenFixture {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TokenFixture")
.field("label", &self.label)
.field("spec", &self.spec)
.finish_non_exhaustive()
}
}
pub trait TokenFactoryExt {
fn token(&self, label: impl AsRef<str>, spec: TokenSpec) -> TokenFixture;
fn token_with_variant(
&self,
label: impl AsRef<str>,
spec: TokenSpec,
variant: impl AsRef<str>,
) -> TokenFixture;
}
impl TokenFactoryExt for Factory {
fn token(&self, label: impl AsRef<str>, spec: TokenSpec) -> TokenFixture {
TokenFixture::new(self.clone(), label.as_ref(), spec)
}
fn token_with_variant(
&self,
label: impl AsRef<str>,
spec: TokenSpec,
variant: impl AsRef<str>,
) -> TokenFixture {
let label = label.as_ref();
let variant = variant.as_ref();
let factory = self.clone();
let inner = load_inner(&factory, label, spec, variant);
TokenFixture {
factory,
label: label.to_string(),
spec,
inner,
}
}
}
impl TokenFixture {
fn new(factory: Factory, label: &str, spec: TokenSpec) -> Self {
let inner = load_inner(&factory, label, spec, "good");
Self {
factory,
label: label.to_string(),
spec,
inner,
}
}
#[allow(
dead_code,
reason = "reserved for future variant-based negative fixtures"
)]
fn load_variant(&self, variant: &str) -> Arc<Inner> {
load_inner(&self.factory, &self.label, self.spec, variant)
}
pub fn spec(&self) -> TokenSpec {
self.spec
}
pub fn label(&self) -> &str {
&self.label
}
pub fn value(&self) -> &str {
&self.inner.value
}
pub fn authorization_header(&self) -> String {
let scheme = self.spec.authorization_scheme();
format!("{scheme} {}", self.value())
}
pub fn negative_value(&self, variant: NegativeToken) -> String {
load_negative_inner(&self.factory, &self.label, self.spec, variant)
.value
.clone()
}
}
fn load_inner(factory: &Factory, label: &str, spec: TokenSpec, variant: &str) -> Arc<Inner> {
let spec_bytes = spec.stable_bytes();
factory.get_or_init(DOMAIN_TOKEN_FIXTURE, label, &spec_bytes, variant, |seed| {
let value = generate_token(label, spec, seed);
Inner { value }
})
}
fn load_negative_inner(
factory: &Factory,
label: &str,
spec: TokenSpec,
variant: NegativeToken,
) -> Arc<Inner> {
let spec_bytes = spec.stable_bytes();
let cache_variant = format!("negative:{}", variant.variant_name());
factory.get_or_init(
DOMAIN_TOKEN_FIXTURE,
label,
&spec_bytes,
&cache_variant,
|seed| {
let value = generate_negative_token(label, spec, seed, variant);
Inner { value }
},
)
}
#[cfg(test)]
mod tests {
use base64::Engine as _;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use super::*;
use uselesskey_core::Seed;
#[test]
fn deterministic_token_is_stable() {
let fx = Factory::deterministic(Seed::from_env_value("token-det").unwrap());
let t1 = fx.token("svc", TokenSpec::api_key());
let t2 = fx.token("svc", TokenSpec::api_key());
assert_eq!(t1.value(), t2.value());
}
#[test]
fn random_mode_still_caches_per_identity() {
let fx = Factory::random();
let t1 = fx.token("svc", TokenSpec::bearer());
let t2 = fx.token("svc", TokenSpec::bearer());
assert_eq!(t1.value(), t2.value());
}
#[test]
fn different_labels_produce_different_tokens() {
let fx = Factory::deterministic(Seed::from_env_value("token-label").unwrap());
let a = fx.token("a", TokenSpec::bearer());
let b = fx.token("b", TokenSpec::bearer());
assert_ne!(a.value(), b.value());
}
#[test]
fn api_key_shape_is_realistic() {
let fx = Factory::random();
let token = fx.token("svc", TokenSpec::api_key());
assert!(token.value().starts_with("uk_test_"));
let suffix = &token.value()["uk_test_".len()..];
assert_eq!(suffix.len(), 32);
assert!(suffix.chars().all(|c| c.is_ascii_alphanumeric()));
}
#[test]
fn bearer_header_uses_bearer_scheme() {
let fx = Factory::random();
let token = fx.token("svc", TokenSpec::bearer());
let header = token.authorization_header();
assert!(header.starts_with("Bearer "));
assert!(header.ends_with(token.value()));
}
#[test]
fn oauth_token_has_three_segments_and_json_header() {
let fx = Factory::deterministic(Seed::from_env_value("token-oauth").unwrap());
let token = fx.token("issuer", TokenSpec::oauth_access_token());
let parts: Vec<&str> = token.value().split('.').collect();
assert_eq!(parts.len(), 3);
let header_bytes = URL_SAFE_NO_PAD
.decode(parts[0])
.expect("decode JWT header segment");
let payload_bytes = URL_SAFE_NO_PAD
.decode(parts[1])
.expect("decode JWT payload segment");
let header: serde_json::Value = serde_json::from_slice(&header_bytes).expect("header json");
let payload: serde_json::Value =
serde_json::from_slice(&payload_bytes).expect("payload json");
assert_eq!(header["alg"], "RS256");
assert_eq!(header["typ"], "JWT");
assert_eq!(payload["sub"], "issuer");
assert_eq!(payload["iss"], "uselesskey");
}
#[test]
fn different_variants_produce_different_tokens() {
let fx = Factory::deterministic(Seed::from_env_value("token-variant").unwrap());
let token = fx.token("svc", TokenSpec::bearer());
let other = token.load_variant("other");
assert_ne!(token.value(), other.value.as_str());
}
#[test]
fn token_with_variant_uses_custom_variant() {
let fx = Factory::deterministic(Seed::from_env_value("token-variant2").unwrap());
let good = fx.token("svc", TokenSpec::api_key());
let custom = fx.token_with_variant("svc", TokenSpec::api_key(), "custom");
assert_ne!(good.value(), custom.value());
}
#[test]
fn negative_value_is_cached_and_stable() {
let fx = Factory::deterministic(Seed::from_env_value("token-negative").unwrap());
let token = fx.token("issuer", TokenSpec::oauth_access_token());
let a = token.negative_value(NegativeToken::ExpiredClaims);
let b = token.negative_value(NegativeToken::ExpiredClaims);
assert_eq!(a, b);
assert_ne!(a, token.value());
assert_eq!(a.matches('.').count(), 2);
}
#[test]
fn negative_api_key_near_miss_keeps_positive_fixture_unchanged() {
let fx = Factory::deterministic(Seed::from_env_value("token-negative-api").unwrap());
let token = fx.token("billing", TokenSpec::api_key());
let near_miss = token.negative_value(NegativeToken::NearMissApiKey);
assert!(token.value().starts_with("uk_test_"));
assert!(near_miss.starts_with("uk_tset_"));
assert!(!near_miss.starts_with("uk_test_"));
assert_ne!(near_miss, token.value());
}
#[test]
fn debug_does_not_include_token_value() {
let fx = Factory::random();
let token = fx.token("debug-label", TokenSpec::api_key());
let dbg = format!("{token:?}");
assert!(dbg.contains("TokenFixture"));
assert!(dbg.contains("debug-label"));
assert!(!dbg.contains(token.value()));
}
#[test]
fn random_base62_uses_full_alphabet() {
let fx = Factory::deterministic(Seed::from_env_value("base62-test").unwrap());
let t = fx.token("alphabet-test", TokenSpec::api_key());
let value = t.value();
let suffix = value.strip_prefix("uk_test_").expect("API key prefix");
assert!(
suffix
.chars()
.any(|c| c.is_ascii_lowercase() || c.is_ascii_digit()),
"random suffix should use full base62 alphabet, got: {suffix}"
);
}
}