use super::*;
use std::time::{Duration, UNIX_EPOCH};
#[test]
fn c13_issued_token_debug_redacts_the_access_token() {
let record = IssuedToken {
grant_established_at: std::time::UNIX_EPOCH,
#[cfg(feature = "dpop")]
jkt: None,
#[cfg(feature = "mtls")]
x5t_s256: None,
#[cfg(feature = "token-exchange")]
act: None,
access_token: "at-secret-value".into(),
client_id: ClientId::new("app"),
subject: Some("alice".into()),
scope: ScopeSet::parse("read write").unwrap(),
resource: vec!["https://rs.example/api".to_string()],
#[cfg(feature = "rar")]
authorization_details: Default::default(),
issued_at: UNIX_EPOCH + Duration::from_secs(1_000),
expires_at: UNIX_EPOCH + Duration::from_secs(4_600),
family_id: Some("fam-1".into()),
#[cfg(feature = "consent")]
authentication: None,
};
let printed = format!("{record:?}");
assert!(
!printed.contains("at-secret-value"),
"debug format leaked the access token: {printed}"
);
assert!(printed.contains("[redacted]"), "{printed}");
for visible in ["app", "alice", "read", "write", "fam-1"] {
assert!(
printed.contains(visible),
"non-secret field {visible} must stay visible: {printed}"
);
}
}
#[test]
fn c13_refresh_token_record_debug_redacts_the_refresh_token() {
let record = RefreshTokenRecord {
grant_established_at: std::time::UNIX_EPOCH,
#[cfg(feature = "dpop")]
jkt: None,
#[cfg(feature = "mtls")]
x5t_s256: None,
refresh_token: "rt-secret-value".into(),
client_id: ClientId::new("app"),
subject: Some("alice".into()),
scope: ScopeSet::parse("read").unwrap(),
resource: vec!["https://rs.example/api".to_string()],
#[cfg(feature = "rar")]
authorization_details: Default::default(),
expires_at: Some(UNIX_EPOCH + Duration::from_secs(9_000)),
family_id: "fam-1".into(),
state: RefreshTokenState::Spent,
#[cfg(feature = "consent")]
authentication: None,
};
let printed = format!("{record:?}");
assert!(
!printed.contains("rt-secret-value"),
"debug format leaked the refresh token: {printed}"
);
assert!(printed.contains("[redacted]"), "{printed}");
for visible in ["app", "alice", "read", "fam-1", "Spent"] {
assert!(
printed.contains(visible),
"non-secret field {visible} must stay visible: {printed}"
);
}
}
#[test]
fn c13_token_response_debug_redacts_both_tokens_and_keeps_option_shape_visible() {
let with_refresh = TokenResponse {
access_token: "at-secret-value".into(),
token_type: TokenType::Bearer,
expires_in: 3600,
refresh_token: Some("rt-secret-value".into()),
scope: Some("read write".into()),
#[cfg(feature = "rar")]
authorization_details: Default::default(),
};
let printed = format!("{with_refresh:?}");
assert!(
!printed.contains("at-secret-value"),
"debug format leaked the access token: {printed}"
);
assert!(
!printed.contains("rt-secret-value"),
"debug format leaked the refresh token: {printed}"
);
assert!(printed.contains("[redacted]"), "{printed}");
for visible in ["3600", "read", "write"] {
assert!(
printed.contains(visible),
"non-secret field {visible} must stay visible: {printed}"
);
}
let without_refresh = TokenResponse {
refresh_token: None,
..with_refresh
};
let printed_without = format!("{without_refresh:?}");
assert_ne!(
printed, printed_without,
"a Some(refresh_token) and a None must not debug-print identically"
);
assert!(
printed_without.contains("None"),
"absent refresh_token must render as None, not as a redacted value: {printed_without}"
);
}
#[test]
fn success_response_shape_is_rfc6749_5_1() {
let full = TokenResponse {
access_token: "at".into(),
token_type: TokenType::Bearer,
expires_in: 3600,
refresh_token: Some("rt".into()),
scope: Some("read write".into()),
#[cfg(feature = "rar")]
authorization_details: Default::default(),
};
assert_eq!(
serde_json::to_value(&full).unwrap(),
serde_json::json!({
"access_token": "at",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "rt",
"scope": "read write",
})
);
let minimal = TokenResponse {
access_token: "at".into(),
token_type: TokenType::Bearer,
expires_in: 60,
refresh_token: None,
scope: None,
#[cfg(feature = "rar")]
authorization_details: Default::default(),
};
assert_eq!(
serde_json::to_value(&minimal).unwrap(),
serde_json::json!({ "access_token": "at", "token_type": "Bearer", "expires_in": 60 }),
"absent optionals must be omitted, not null"
);
}