#[test]
fn error_code_is_non_exhaustive() {
let source = include_str!("../src/error.rs");
let at = source
.find("pub enum ErrorCode")
.expect("the enum has to still be there");
let preamble = &source[at.saturating_sub(400)..at];
assert!(
preamble.contains("#[non_exhaustive]"),
"ErrorCode gains variants with a cargo feature, so a host's exhaustive match must not \
break on a feature flag"
);
}
#[test]
fn error_code_http_status_chooses_a_status_for_every_variant() {
let source = include_str!("../src/error.rs");
let at = source
.find("pub fn http_status(self) -> u16 {")
.expect("the method has to still be there");
let body = &source[at..];
let end = body.find("\n }").expect("the method has to end");
assert!(
!body[..end].contains("_ =>"),
"http_status must match every variant explicitly, so that adding one forces the author \
to choose its status rather than inheriting 400"
);
}
#[test]
fn registration_error_response_is_a_std_error() {
fn as_boxed_error<E: std::error::Error + Send + Sync + 'static>(
e: E,
) -> Box<dyn std::error::Error> {
Box::new(e)
}
let refusal = oauth_as::RegistrationErrorResponse::new(
oauth_as::RegistrationErrorCode::InvalidRedirectUri,
"redirect_uri must be an absolute URI",
);
let text = refusal.to_string();
let boxed = as_boxed_error(refusal);
assert_eq!(boxed.to_string(), text);
}
#[test]
fn must_use_on_consuming_builders_is_all_or_nothing() {
let src_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src");
let mut marked = Vec::new();
let mut unmarked = Vec::new();
let mut files: Vec<std::path::PathBuf> = std::fs::read_dir(src_dir)
.expect("the crate's src/ must be readable")
.map(|e| e.expect("a readable directory entry").path())
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("rs"))
.collect();
files.sort();
for path in files {
let text = std::fs::read_to_string(&path).expect("a readable source file");
let lines: Vec<&str> = text.lines().collect();
for (i, line) in lines.iter().enumerate() {
if !line.trim_start().starts_with("pub fn ") {
continue;
}
let mut signature = line.trim_start().to_string();
let mut j = i;
while !signature.contains('{') && j + 1 < lines.len() {
j += 1;
signature.push(' ');
signature.push_str(lines[j].trim_start());
}
let signature = signature.split('{').next().unwrap_or("").to_string();
let packed: String = signature.chars().filter(|c| !c.is_whitespace()).collect();
let consumes = packed.contains("(mutself")
|| packed.contains("(self,")
|| packed.contains("(self)");
if !consumes || !packed.contains("->Self") {
continue;
}
let has_must_use = lines[..i]
.iter()
.rev()
.take_while(|prev| {
let p = prev.trim_start();
p.starts_with("///") || p.starts_with("#[") || p.starts_with("//")
})
.any(|prev| prev.trim_start().starts_with("#[must_use"));
let site = format!("{}:{}: {}", path.display(), i + 1, signature.trim());
if has_must_use {
marked.push(site);
} else {
unmarked.push(site);
}
}
}
assert!(
!marked.is_empty() || !unmarked.is_empty(),
"the scan found no consuming builders at all, so it has stopped testing anything"
);
assert!(
marked.is_empty() || unmarked.is_empty(),
"#[must_use] on consuming builders must be all or nothing; {} of {} carry it:\n{}\n\
and these do not:\n{}",
marked.len(),
marked.len() + unmarked.len(),
marked.join("\n"),
unmarked.join("\n")
);
}
#[test]
fn every_public_request_cap_is_reexported_at_the_crate_root() {
#[allow(unused_mut)]
let mut caps: Vec<(&str, usize)> = vec![
("MIN_USER_CODE_LENGTH", oauth_as::MIN_USER_CODE_LENGTH),
("MAX_RESOURCE_INDICATORS", oauth_as::MAX_RESOURCE_INDICATORS),
(
"MAX_REGISTERED_REDIRECT_URIS",
oauth_as::MAX_REGISTERED_REDIRECT_URIS,
),
];
#[cfg(feature = "rar")]
caps.extend([
(
"MAX_AUTHORIZATION_DETAILS_BYTES",
oauth_as::MAX_AUTHORIZATION_DETAILS_BYTES,
),
(
"MAX_AUTHORIZATION_DETAILS_ELEMENTS",
oauth_as::MAX_AUTHORIZATION_DETAILS_ELEMENTS,
),
(
"MAX_AUTHORIZATION_DETAILS_DEPTH",
oauth_as::MAX_AUTHORIZATION_DETAILS_DEPTH,
),
]);
#[cfg(feature = "token-exchange")]
caps.push(("MAX_AUDIENCE_VALUES", oauth_as::MAX_AUDIENCE_VALUES));
#[cfg(feature = "consent")]
caps.push(("MAX_CONSENT_RESOURCES", oauth_as::MAX_CONSENT_RESOURCES));
#[cfg(feature = "dpop")]
caps.push(("MAX_PROOF_BYTES", oauth_as::MAX_PROOF_BYTES));
#[cfg(feature = "http")]
caps.extend([
("MAX_FORM_PARAMETERS", oauth_as::MAX_FORM_PARAMETERS),
("MAX_BODY_BYTES", oauth_as::MAX_BODY_BYTES),
]);
for (name, value) in &caps {
assert!(*value > 0, "{name} must be a positive cap");
}
#[cfg(feature = "token-exchange")]
{
let audience = caps
.iter()
.find(|(n, _)| *n == "MAX_AUDIENCE_VALUES")
.expect("it was just pushed");
let resource = caps
.iter()
.find(|(n, _)| *n == "MAX_RESOURCE_INDICATORS")
.expect("it is unconditional");
assert_eq!(
audience.1, resource.1,
"RFC 8693 s2.1.1 makes audience and resource two spellings of one thing, and this \
crate holds them to one number"
);
}
}