#[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 every_feature_varying_public_type_is_non_exhaustive() {
const DELIBERATELY_UNMARKED: &[(&str, &str)] = &[
(
"ValidatedAuthorizationRequest",
"sealed by a private zero-sized field, so it is already unconstructible and \
un-destructurable from outside this crate; see its own doc comment",
),
(
"ServiceBuilder",
"every field is private, so the attribute would add nothing: a host builds it with \
new() and the with_* seams and can name none of its insides",
),
(
"AuthorizationServer",
"every field is private, for the same reason as ServiceBuilder. Its `token_endpoint` \
is derived under client-assertion or dpop and is not something a host may ever spell",
),
];
let src_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/src");
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();
let mut varying = Vec::new();
let mut offenders = Vec::new();
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() {
let trimmed = line.trim_start();
let rest = match trimmed
.strip_prefix("pub struct ")
.or_else(|| trimmed.strip_prefix("pub enum "))
{
Some(rest) => rest,
None => continue,
};
let name: String = rest
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if name.is_empty() {
continue;
}
let mut depth = 0usize;
let mut started = false;
let mut body = Vec::new();
for body_line in lines[i..].iter() {
let t = body_line.trim_start();
let is_prose = t.starts_with("///") || t.starts_with("//");
if !is_prose {
if !started {
if let Some(stop) = body_line.find(['{', '(', ';']) {
if body_line.as_bytes()[stop] != b'{' {
break;
}
started = true;
}
}
if started {
depth += body_line.matches('{').count();
depth -= body_line.matches('}').count().min(depth);
}
}
if started {
body.push(*body_line);
if depth == 0 {
break;
}
}
}
if !started {
continue;
}
let varies = body.iter().any(|b| {
let t = b.trim_start();
t.starts_with("#[cfg(") && t.contains("feature")
});
if !varies {
continue;
}
varying.push(name.clone());
let marked = 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("#[non_exhaustive]"));
if marked || DELIBERATELY_UNMARKED.iter().any(|(n, _)| *n == name) {
continue;
}
offenders.push(format!("{}:{}: {}", path.display(), i + 1, name));
}
}
assert!(
varying.len() > 10,
"the scan found only {} feature-varying public types, so it has stopped finding them and \
is no longer testing anything",
varying.len()
);
for (name, why) in DELIBERATELY_UNMARKED {
assert!(
varying.iter().any(|v| v == name),
"{name} is on the deliberately-unmarked list ({why}) but the scan no longer finds it \
varying with a feature; drop the entry rather than leaving it to excuse a type that \
has changed shape since"
);
}
assert!(
offenders.is_empty(),
"these public types gain a field or a variant with a cargo feature and are not \
#[non_exhaustive], so a host's struct literal or exhaustive match against them breaks \
when anything in their dependency graph enables a feature they did not ask for:\n{}",
offenders.join("\n")
);
}
#[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);
}
}
}
let found = marked.len() + unmarked.len();
assert!(
found > 10,
"the scan found only {found} consuming builders, so it has stopped finding them and is no \
longer 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_type_in_a_storage_signature_is_reexported_at_the_crate_root() {
const NOT_OURS: &[&str] = &[
"Future",
"Output",
"Result",
"Option",
"Some",
"None",
"Send",
"Sync",
"Sized",
"Arc",
"Vec",
"String",
"SystemTime",
"Duration",
"Self",
];
let store_src = include_str!("../src/store.rs");
let lib_src = include_str!("../src/lib.rs");
let trait_at = store_src
.find("pub trait Storage: Send + Sync {")
.expect("the trait has to still be there");
let trait_end = store_src[trait_at..]
.find("\n}\n")
.expect("the trait has to end")
+ trait_at;
let body = &store_src[trait_at..trait_end];
let mut reexports = String::new();
let mut in_use = false;
for line in lib_src.lines() {
if line.starts_with("pub use ") {
in_use = true;
}
if in_use {
reexports.push_str(line);
reexports.push('\n');
if line.trim_end().ends_with(';') {
in_use = false;
}
}
}
let mut named = Vec::new();
let mut missing = Vec::new();
let mut signature = String::new();
for line in body.lines() {
let trimmed = line.trim();
if trimmed.starts_with("///") || trimmed.starts_with("//") {
continue;
}
if signature.is_empty() && !line.starts_with(" fn ") {
continue;
}
signature.push(' ');
signature.push_str(trimmed);
if !trimmed.ends_with(';') {
continue;
}
for word in signature.split(|c: char| !(c.is_alphanumeric() || c == '_')) {
let mut chars = word.chars();
let Some(first) = chars.next() else { continue };
if !first.is_ascii_uppercase() || NOT_OURS.contains(&word) {
continue;
}
if !named.iter().any(|n| n == word) {
named.push(word.to_string());
}
let exported = reexports
.split(|c: char| !(c.is_alphanumeric() || c == '_'))
.any(|t| t == word);
if !exported && !missing.iter().any(|m| m == word) {
missing.push(word.to_string());
}
}
signature.clear();
}
assert!(
named.len() > 8,
"the scan found only {} crate-defined types in Storage signatures, so it has stopped \
finding them and is no longer testing anything: {named:?}",
named.len()
);
assert!(
missing.is_empty(),
"these types appear in a `Storage` method signature and are NOT re-exported at the crate \
root, so a host cannot write the impl from `oauth_as::` alone: {missing:?}\n\
A re-export that is missing is an absence rather than an error, invisible from inside this \
crate; add each to the `pub use` list in lib.rs under the SAME `#[cfg]` its item carries."
);
}
#[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"
);
}
}