use super::*;
#[test]
fn user_codes_use_the_alphabet_and_are_unbiased_in_shape() {
let code = random_user_code(8);
assert_eq!(code.len(), 8);
assert!(code.bytes().all(|b| USER_CODE_ALPHABET.contains(&b)));
}
#[test]
fn display_form_hyphenates_even_lengths() {
assert_eq!(display_user_code("WDJBMJHT"), "WDJB-MJHT");
assert_eq!(display_user_code("ABCDEF"), "ABC-DEF");
assert_eq!(display_user_code("ABCDE"), "ABCDE");
}
#[test]
fn the_user_code_symbol_draw_is_exactly_uniform_over_the_alphabet() {
let mut counts = std::collections::BTreeMap::new();
let mut accepted = 0usize;
for byte in 0u8..=255 {
match user_code_symbol(byte) {
Some(symbol) => {
accepted += 1;
assert!(
USER_CODE_ALPHABET.contains(&symbol),
"byte {byte} produced {symbol}, which is outside the RFC 8628 s6.1 alphabet"
);
*counts.entry(symbol).or_insert(0usize) += 1;
}
None => assert!(
byte >= USER_CODE_REJECT_AT,
"byte {byte} is below the rejection bound and must have been accepted"
),
}
}
assert_eq!(
accepted, 240,
"exactly the 240 values below the rejection bound may be folded into the alphabet"
);
assert_eq!(
counts.len(),
USER_CODE_ALPHABET.len(),
"every symbol in the alphabet must be reachable"
);
for (symbol, count) in &counts {
assert_eq!(
*count, 12,
"symbol {} has {count} preimages, not the uniform 12: the draw is biased",
*symbol as char
);
}
}
#[test]
fn random_user_code_redraws_rejections_rather_than_shortening_the_code() {
for len in [MIN_USER_CODE_LENGTH, 9, 16] {
let code = random_user_code(len);
assert_eq!(code.len(), len, "a rejected byte must cost a redraw");
assert!(code.bytes().all(|b| USER_CODE_ALPHABET.contains(&b)));
}
}
#[test]
fn random_hex_has_the_stated_entropy_width() {
let h = random_hex(32);
assert_eq!(h.len(), 64);
assert!(h.bytes().all(|b| b.is_ascii_hexdigit()));
assert_ne!(random_hex(32), random_hex(32));
}
#[test]
fn c13_token_request_debug_redacts_every_credential() {
let cases = vec![
TokenRequest::AuthorizationCode {
client_id: ClientId::new("app"),
client_secret: Some("secret-value".into()),
code: "code-value".into(),
redirect_uri: Some("https://app.example/cb".into()),
code_verifier: Some("verifier-value".into()),
},
TokenRequest::ClientCredentials {
client_id: ClientId::new("app"),
client_secret: Some("secret-value".into()),
scope: None,
},
TokenRequest::DeviceCode {
client_id: ClientId::new("app"),
client_secret: Some("secret-value".into()),
device_code: "device-value".into(),
},
TokenRequest::RefreshToken {
client_id: ClientId::new("app"),
client_secret: Some("secret-value".into()),
refresh_token: "refresh-value".into(),
scope: None,
},
];
for request in &cases {
let printed = format!("{request:?}");
for leaked in [
"secret-value",
"code-value",
"verifier-value",
"device-value",
"refresh-value",
] {
assert!(
!printed.contains(leaked),
"debug format leaked {leaked}: {printed}"
);
}
assert!(
printed.contains("[redacted]"),
"debug format should say what was redacted: {printed}"
);
assert!(
printed.contains("app"),
"client_id must stay visible: {printed}"
);
}
}
#[test]
fn c13_token_request_debug_keeps_the_some_none_distinction() {
let with_secret = TokenRequest::AuthorizationCode {
client_id: ClientId::new("app"),
client_secret: Some("secret-value".into()),
code: "code-value".into(),
redirect_uri: None,
code_verifier: Some("verifier-value".into()),
};
let without_secret = TokenRequest::AuthorizationCode {
client_id: ClientId::new("app"),
client_secret: None,
code: "code-value".into(),
redirect_uri: None,
code_verifier: None,
};
let with = format!("{with_secret:?}");
let without = format!("{without_secret:?}");
assert_ne!(
with, without,
"a presented secret and an absent one must not debug-print identically"
);
assert!(with.contains("Some(\"[redacted]\")"), "{with}");
assert!(without.contains("client_secret: None"), "{without}");
assert!(without.contains("code_verifier: None"), "{without}");
}
#[test]
fn c13_token_request_debug_still_names_the_grant() {
let request = TokenRequest::RefreshToken {
client_id: ClientId::new("app"),
client_secret: None,
refresh_token: "refresh-value".into(),
scope: Some(ScopeSet::parse("read").unwrap()),
};
let printed = format!("{request:?}");
assert!(printed.starts_with("RefreshToken"), "{printed}");
assert!(printed.contains("read"), "{printed}");
}
#[cfg(any(feature = "client_assertion", feature = "dpop"))]
#[test]
fn a_replay_key_separates_its_three_parts() {
assert_eq!(replay_key("ca", "client-1", "jti-1"), "ca:8:client-1jti-1");
assert_eq!(replay_key("dpop", "thumb", "jti-1"), "dpop:5:thumbjti-1");
assert_ne!(replay_key("ca", "x", "j"), replay_key("dpop", "x", "j"));
assert_ne!(replay_key("ca", "ab", "c"), replay_key("ca", "a", "bc"));
assert_ne!(
replay_key("ca", "urn", "client:foo:42"),
replay_key("ca", "urn:client:foo", "42")
);
assert_ne!(
replay_key("dpop", "thumb", ":x"),
replay_key("dpop", "thumb:", "x")
);
}
#[cfg(any(feature = "client_assertion", feature = "dpop"))]
#[test]
fn the_decimal_width_is_the_number_of_digits() {
for (n, width) in [
(0usize, 1usize),
(1, 1),
(9, 1),
(10, 2),
(99, 2),
(100, 3),
(999, 3),
(1000, 4),
] {
assert_eq!(decimal_width(n), width, "{n}");
}
}
#[cfg(any(feature = "client_assertion", feature = "dpop"))]
#[test]
fn a_replay_key_is_built_in_exactly_one_correctly_sized_allocation() {
for (kind, owner, jti) in [
("ca", "client-1", "jti-1"),
("dpop", "0OXy9SbXe0Y7YQ8Xw3sYQ2h1lKQ", "01234567-89ab-cdef"),
("ca", "", ""),
] {
let key = replay_key(kind, owner, jti);
let exact = kind.len() + owner.len() + jti.len() + 2 + decimal_width(owner.len());
assert_eq!(
key.len(),
exact,
"the two separators and the length prefix are the whole of the difference between \
the parts and the key"
);
assert_eq!(
key.capacity(),
exact,
"the hint must be exactly the final length: smaller reallocates, larger over-asks"
);
}
}