#[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")
);
}
fn reexport_surface(lib_src: &str) -> String {
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;
}
}
}
reexports
}
fn names_word(haystack: &str, name: &str) -> bool {
haystack
.split(|c: char| !(c.is_alphanumeric() || c == '_'))
.any(|t| t == name)
}
#[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 reexports = reexport_surface(lib_src);
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 = names_word(&reexports, 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() {
const MODULE_ONLY: &[(&str, &str)] = &[
("MAX_ACT_CHAIN_DEPTH", "token_exchange"),
("MAX_ASSERTION_BYTES", "client_assertion"),
("MAX_ACR_VALUES", "consent"),
("MAX_DETAIL_LIST_ENTRIES", "rar"),
("MAX_JTI_BYTES", "dpop"),
];
let lib_src = include_str!("../src/lib.rs");
let reexports = reexport_surface(lib_src);
let src_dir = std::path::PathBuf::from(concat!(env!("CARGO_MANIFEST_DIR"), "/src"));
let mut files: Vec<std::path::PathBuf> = Vec::new();
let mut stack = vec![src_dir.clone()];
while let Some(dir) = stack.pop() {
for entry in std::fs::read_dir(&dir).expect("the crate's src/ must be readable") {
let path = entry.expect("a readable directory entry").path();
if path.is_dir() {
if path.file_name().and_then(|n| n.to_str()) != Some("tests") {
stack.push(path);
}
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
files.push(path);
}
}
}
files.sort();
let mut caps: Vec<(String, String)> = Vec::new();
let mut scanned_modules = 0_usize;
for path in &files {
let stem = path
.file_stem()
.and_then(|s| s.to_str())
.expect("a source file has a name");
if !lib_src.contains(&format!("pub mod {stem};")) {
continue;
}
scanned_modules += 1;
let text = std::fs::read_to_string(path).expect("a readable source file");
for line in text.lines() {
let trimmed = line.trim_start();
let Some(rest) = trimmed.strip_prefix("pub const ") else {
continue;
};
let Some((name, ty)) = rest.split_once(':') else {
continue;
};
let ty = ty.trim_start();
if !(ty.starts_with("usize") || ty.starts_with("u32")) {
continue;
}
if !(name.starts_with("MAX_") || name.starts_with("MIN_")) {
continue;
}
caps.push((name.to_string(), stem.to_string()));
}
}
assert!(
caps.len() >= 15 && scanned_modules >= 8,
"the scan found only {} caps across {scanned_modules} public modules, so it has stopped \
finding them and is no longer testing anything: {caps:?}",
caps.len()
);
let mut missing: Vec<String> = Vec::new();
for (name, module) in &caps {
if names_word(&reexports, name) {
continue;
}
if MODULE_ONLY.iter().any(|(excused, _)| excused == name) {
continue;
}
missing.push(format!("{name} (src/{module}.rs)"));
}
assert!(
missing.is_empty(),
"these caps are public on their module and NOT re-exported at the crate root, so a host \
sizing its own gateway cannot reach them from `oauth_as::` alone: {missing:?}\n\
Add each to the `pub use` list in lib.rs under the SAME `#[cfg]` its item carries, or, if \
it genuinely belongs to its module only, add it to MODULE_ONLY in this test with the \
reason."
);
for (excused, module) in MODULE_ONLY {
assert!(
caps.iter()
.any(|(name, found_in)| name == excused && found_in == module),
"MODULE_ONLY names {excused} in src/{module}.rs, and the scan does not find it there. \
It was renamed, moved or deleted: fix the entry rather than leaving a dead excuse in \
the list."
);
assert!(
!names_word(&reexports, excused),
"MODULE_ONLY says {excused} is reachable only through `oauth_as::{module}::`, but \
lib.rs now re-exports it at the root. Delete the entry: the exception is spent."
);
}
#[cfg(feature = "token-exchange")]
assert_eq!(
oauth_as::MAX_AUDIENCE_VALUES,
oauth_as::MAX_RESOURCE_INDICATORS,
"RFC 8693 s2.1.1 makes audience and resource two spellings of one thing, and this crate \
holds them to one number"
);
}