use super::*;
#[test]
fn wire_spellings() {
assert_eq!(
serde_json::to_value(ResponseType::Code).unwrap(),
serde_json::json!("code")
);
assert_eq!(
serde_json::to_value(CodeChallengeMethod::S256).unwrap(),
serde_json::json!("S256")
);
}
#[test]
fn implicit_grant_is_not_representable() {
assert!(serde_json::from_value::<ResponseType>(serde_json::json!("token")).is_err());
assert!(serde_json::from_value::<CodeChallengeMethod>(serde_json::json!("plain")).is_err());
}
#[test]
fn c13_authorization_code_record_debug_format_redacts_code_and_tokens() {
let record = AuthorizationCodeRecord {
issued_at: std::time::UNIX_EPOCH,
code: "the-secret-code-value".to_string(),
client_id: crate::client::ClientId::new("some-client"),
redirect_uri: "https://registered.example/callback".to_string(),
redirect_uri_was_explicit: true,
scope: crate::scope::ScopeSet::parse("read").unwrap(),
subject: "user-1".to_string(),
code_challenge: "some-challenge".to_string(),
code_challenge_method: CodeChallengeMethod::S256,
resource: vec!["https://rs.example/api".to_string()],
#[cfg(feature = "rar")]
authorization_details: Default::default(),
expires_at: std::time::SystemTime::UNIX_EPOCH,
state: AuthorizationCodeState::Consumed {
access_token: Some("the-secret-access-token".to_string()),
refresh_token: Some("the-secret-refresh-token".to_string()),
},
#[cfg(feature = "consent")]
authentication: None,
};
let printed = format!("{record:?}");
assert!(!printed.contains("the-secret-code-value"), "{printed}");
assert!(!printed.contains("the-secret-access-token"), "{printed}");
assert!(!printed.contains("the-secret-refresh-token"), "{printed}");
assert!(printed.contains("some-client"), "{printed}");
assert!(printed.contains("registered.example"), "{printed}");
assert!(printed.contains("user-1"), "{printed}");
}
#[test]
fn c13_issued_state_debug_format_has_nothing_to_redact() {
assert_eq!(format!("{:?}", AuthorizationCodeState::Issued), "Issued");
}
#[test]
fn c13_consumed_state_without_refresh_token_shows_none_not_a_value() {
let state = AuthorizationCodeState::Consumed {
access_token: Some("at".to_string()),
refresh_token: None,
};
let printed = format!("{state:?}");
assert!(!printed.contains("\"at\""), "{printed}");
assert!(printed.contains("None"), "{printed}");
}
#[test]
fn c11_only_the_sealed_constructor_produces_a_validated_request() {
let validated = ValidatedAuthorizationRequest::new(
crate::client::ClientId::new("victim-client"),
"https://registered.example/callback".to_string(),
true,
crate::scope::ScopeSet::parse("read").unwrap(),
None,
"some-challenge".to_string(),
CodeChallengeMethod::S256,
"https://as.example".to_string(),
Vec::new(),
);
assert_eq!(
validated.redirect_uri,
"https://registered.example/callback"
);
}
#[test]
fn rfc8707_resource_indicators_must_be_absolute_uris_without_a_fragment() {
for ok in [
"https://rs.example",
"https://rs.example/api",
"https://rs.example/api?tenant=1",
"urn:example:resource",
"coap+tcp://rs.example/x",
] {
assert!(
is_valid_resource_indicator(ok),
"RFC 8707 s2 admits {ok} as a resource indicator"
);
}
for bad in [
"",
"/api",
"rs.example/api",
"//rs.example/api",
":no-scheme",
"1http://rs.example",
"ht_tp://rs.example",
"https://rs.example/#",
"https://rs.example/api#section",
"https://rs.example/api?q=1#f",
"https://rs.example/a b",
"https://rs.example/\u{e9}",
] {
assert!(
!is_valid_resource_indicator(bad),
"RFC 8707 s2 must refuse {bad:?} as a resource indicator"
);
}
}
#[test]
fn rfc8707_resource_is_the_one_repeatable_authorization_parameter() {
let req = AuthorizationRequest::from_pairs([
("client_id", "app"),
("resource", "https://a.example"),
("client_id", "attacker"),
("resource", "https://b.example"),
]);
assert_eq!(
req.resource,
vec!["https://a.example", "https://b.example"],
"every resource occurrence is part of the request (RFC 8707 s2)"
);
assert_eq!(
req.client_id.as_deref(),
Some("app"),
"every OTHER parameter still keeps the first occurrence (RFC 6749 s3.1)"
);
}
#[test]
fn authorization_response_debug_format_redacts_the_code() {
let response = AuthorizationResponse {
code: "the-secret-authorization-code".to_string(),
state: Some("client-state-xyz".to_string()),
iss: "https://as.example".to_string(),
};
let printed = format!("{response:?}");
assert!(
printed.contains("AuthorizationResponse"),
"an emptied Debug names nothing: {printed}"
);
assert!(printed.contains("[redacted]"), "{printed}");
assert!(
!printed.contains("the-secret-authorization-code"),
"the code is a bearer credential and must not print: {printed}"
);
assert!(printed.contains("client-state-xyz"), "{printed}");
assert!(printed.contains("as.example"), "{printed}");
}
#[test]
fn a_code_record_missing_redirect_uri_was_explicit_defaults_to_fail_closed_true() {
let record = AuthorizationCodeRecord {
issued_at: std::time::UNIX_EPOCH,
code: "code-value".to_string(),
client_id: crate::client::ClientId::new("some-client"),
redirect_uri: "https://registered.example/callback".to_string(),
redirect_uri_was_explicit: true,
scope: crate::scope::ScopeSet::parse("read").unwrap(),
subject: "user-1".to_string(),
code_challenge: "some-challenge".to_string(),
code_challenge_method: CodeChallengeMethod::S256,
resource: vec!["https://rs.example/api".to_string()],
#[cfg(feature = "rar")]
authorization_details: Default::default(),
expires_at: std::time::SystemTime::UNIX_EPOCH,
state: AuthorizationCodeState::Consumed {
access_token: Some("at".to_string()),
refresh_token: None,
},
#[cfg(feature = "consent")]
authentication: None,
};
let mut value = serde_json::to_value(&record).expect("the record serializes");
let removed = value
.as_object_mut()
.expect("a record serializes to a JSON object")
.remove("redirect_uri_was_explicit");
assert!(
removed.is_some(),
"the field must be present to model a record written before it existed"
);
let restored: AuthorizationCodeRecord =
serde_json::from_value(value).expect("a pre-field record still deserializes");
assert!(
restored.redirect_uri_was_explicit,
"a record with no stated decision must default to the fail-closed TRUE that keeps \
RFC 6749 s4.1.3's required-parameter check"
);
}