#[cfg(all(not(feature = "std"), feature = "alloc", test))]
use alloc::vec::Vec;
#[cfg(all(not(feature = "std"), feature = "alloc"))]
use alloc::{
borrow::ToOwned,
collections::BTreeSet,
format,
string::{String, ToString},
};
use core::{fmt, str::FromStr};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "std")]
use std::collections::BTreeSet;
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "scheme", content = "value"))]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum AuthScheme {
Bearer,
Basic,
ApiKey,
OAuth2,
Digest,
Custom(String),
}
impl fmt::Display for AuthScheme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bearer => f.write_str("Bearer"),
Self::Basic => f.write_str("Basic"),
Self::ApiKey => f.write_str("ApiKey"),
Self::OAuth2 => f.write_str("OAuth2"),
Self::Digest => f.write_str("Digest"),
Self::Custom(s) => f.write_str(s),
}
}
}
impl FromStr for AuthScheme {
type Err = core::convert::Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"Bearer" | "bearer" => Self::Bearer,
"Basic" | "basic" => Self::Basic,
"ApiKey" | "apikey" | "APIKEY" => Self::ApiKey,
"OAuth2" | "oauth2" | "OAuth" => Self::OAuth2,
"Digest" | "digest" => Self::Digest,
other => Self::Custom(other.to_owned()),
})
}
}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BearerToken(String);
impl BearerToken {
pub fn new(token: impl Into<String>) -> Self {
Self(token.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for BearerToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("BearerToken").field(&"[REDACTED]").finish()
}
}
impl fmt::Display for BearerToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl PartialEq for BearerToken {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for BearerToken {}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct BasicCredentials {
username: String,
password: String,
}
impl BasicCredentials {
pub fn new(username: impl Into<String>, password: impl Into<String>) -> Self {
Self {
username: username.into(),
password: password.into(),
}
}
#[must_use]
pub fn username(&self) -> &str {
&self.username
}
#[must_use]
pub fn password(&self) -> &str {
&self.password
}
}
impl fmt::Debug for BasicCredentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BasicCredentials")
.field("username", &self.username)
.field("password", &"[REDACTED]")
.finish()
}
}
impl PartialEq for BasicCredentials {
fn eq(&self, other: &Self) -> bool {
self.username == other.username && self.password == other.password
}
}
impl Eq for BasicCredentials {}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ApiKeyCredentials(String);
impl ApiKeyCredentials {
pub fn new(key: impl Into<String>) -> Self {
Self(key.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Debug for ApiKeyCredentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("ApiKeyCredentials")
.field(&"[REDACTED]")
.finish()
}
}
impl PartialEq for ApiKeyCredentials {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for ApiKeyCredentials {}
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct OAuth2Token {
access_token: String,
token_type: Option<String>,
}
impl OAuth2Token {
pub fn new(access_token: impl Into<String>, token_type: Option<impl Into<String>>) -> Self {
Self {
access_token: access_token.into(),
token_type: token_type.map(Into::into),
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.access_token
}
#[must_use]
pub fn token_type(&self) -> Option<&str> {
self.token_type.as_deref()
}
}
impl fmt::Debug for OAuth2Token {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OAuth2Token")
.field("access_token", &"[REDACTED]")
.field("token_type", &self.token_type)
.finish()
}
}
impl PartialEq for OAuth2Token {
fn eq(&self, other: &Self) -> bool {
self.access_token == other.access_token && self.token_type == other.token_type
}
}
impl Eq for OAuth2Token {}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ParseAuthorizationError {
#[error("authorization header is empty")]
Empty,
#[error("authorization header is missing the credentials after the scheme")]
MissingCredentials,
#[error("basic credentials are not valid base64: {0}")]
InvalidBase64(String),
#[error("basic credentials must contain a ':' separator between username and password")]
InvalidBasicFormat,
#[error("basic credentials are not valid UTF-8")]
InvalidUtf8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AuthorizationHeader {
Bearer(BearerToken),
Basic(BasicCredentials),
ApiKey(ApiKeyCredentials),
OAuth2(OAuth2Token),
Other {
scheme: String,
credentials: String,
},
}
impl AuthorizationHeader {
#[must_use]
pub fn scheme(&self) -> AuthScheme {
match self {
Self::Bearer(_) => AuthScheme::Bearer,
Self::Basic(_) => AuthScheme::Basic,
Self::ApiKey(_) => AuthScheme::ApiKey,
Self::OAuth2(_) => AuthScheme::OAuth2,
Self::Other { scheme, .. } => AuthScheme::Custom(scheme.clone()),
}
}
}
impl fmt::Display for AuthorizationHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Bearer(tok) => write!(f, "Bearer {}", tok.as_str()),
Self::Basic(creds) => {
use base64::Engine as _;
let plain = format!("{}:{}", creds.username(), creds.password());
let encoded = base64::engine::general_purpose::STANDARD.encode(plain.as_bytes());
write!(f, "Basic {encoded}")
}
Self::ApiKey(key) => write!(f, "ApiKey {}", key.as_str()),
Self::OAuth2(tok) => write!(f, "OAuth2 {}", tok.as_str()),
Self::Other {
scheme,
credentials,
} => write!(f, "{scheme} {credentials}"),
}
}
}
impl FromStr for AuthorizationHeader {
type Err = ParseAuthorizationError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
return Err(ParseAuthorizationError::Empty);
}
let (scheme_str, credentials) = s
.split_once(' ')
.ok_or(ParseAuthorizationError::MissingCredentials)?;
let credentials = credentials.trim();
if credentials.is_empty() {
return Err(ParseAuthorizationError::MissingCredentials);
}
match scheme_str {
"Bearer" | "bearer" => Ok(Self::Bearer(BearerToken::new(credentials))),
"Basic" | "basic" => {
use base64::Engine as _;
let decoded = base64::engine::general_purpose::STANDARD
.decode(credentials.as_bytes())
.map_err(|e| ParseAuthorizationError::InvalidBase64(e.to_string()))?;
let plain = core::str::from_utf8(&decoded)
.map_err(|_| ParseAuthorizationError::InvalidUtf8)?;
let (user, pass) = plain
.split_once(':')
.ok_or(ParseAuthorizationError::InvalidBasicFormat)?;
Ok(Self::Basic(BasicCredentials::new(user, pass)))
}
"ApiKey" | "apikey" | "APIKEY" => Ok(Self::ApiKey(ApiKeyCredentials::new(credentials))),
"OAuth2" | "oauth2" | "OAuth" => {
Ok(Self::OAuth2(OAuth2Token::new(credentials, None::<String>)))
}
other => Ok(Self::Other {
scheme: other.to_owned(),
credentials: credentials.to_owned(),
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Permission(String);
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ParsePermissionError {
#[error("permission must not be empty")]
Empty,
#[error("permission must not contain whitespace")]
ContainsWhitespace,
}
impl Permission {
pub fn new(s: impl AsRef<str>) -> Result<Self, ParsePermissionError> {
let s = s.as_ref();
if s.is_empty() {
return Err(ParsePermissionError::Empty);
}
if s.chars().any(|c| c.is_ascii_whitespace()) {
return Err(ParsePermissionError::ContainsWhitespace);
}
Ok(Self(s.to_owned()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for Permission {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for Permission {
type Err = ParsePermissionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(try_from = "String", into = "String"))]
pub struct Scope(BTreeSet<Permission>);
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("invalid scope token: {0}")]
pub struct ParseScopeError(#[from] ParsePermissionError);
impl Scope {
#[must_use]
pub fn empty() -> Self {
Self(BTreeSet::new())
}
pub fn from_permissions(perms: impl IntoIterator<Item = Permission>) -> Self {
Self(perms.into_iter().collect())
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn contains(&self, perm: &str) -> bool {
self.0.iter().any(|p| p.as_str() == perm)
}
#[must_use]
pub fn is_subset_of(&self, other: &Self) -> bool {
self.0.iter().all(|p| other.0.contains(p))
}
#[must_use]
pub fn union(&self, other: &Self) -> Self {
Self(self.0.iter().chain(other.0.iter()).cloned().collect())
}
#[must_use]
pub fn intersection(&self, other: &Self) -> Self {
Self(
self.0
.iter()
.filter(|p| other.0.contains(*p))
.cloned()
.collect(),
)
}
pub fn iter(&self) -> impl Iterator<Item = &Permission> {
self.0.iter()
}
}
impl fmt::Display for Scope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true;
for p in &self.0 {
if !first {
f.write_str(" ")?;
}
f.write_str(p.as_str())?;
first = false;
}
Ok(())
}
}
impl FromStr for Scope {
type Err = ParseScopeError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
return Ok(Self::empty());
}
let perms = s
.split_ascii_whitespace()
.map(Permission::new)
.collect::<Result<BTreeSet<_>, _>>()?;
Ok(Self(perms))
}
}
impl From<Scope> for String {
fn from(s: Scope) -> Self {
s.to_string()
}
}
impl TryFrom<String> for Scope {
type Error = ParseScopeError;
fn try_from(s: String) -> Result<Self, Self::Error> {
s.parse()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_scheme_display() {
assert_eq!(AuthScheme::Bearer.to_string(), "Bearer");
assert_eq!(AuthScheme::Basic.to_string(), "Basic");
assert_eq!(AuthScheme::ApiKey.to_string(), "ApiKey");
assert_eq!(AuthScheme::OAuth2.to_string(), "OAuth2");
assert_eq!(AuthScheme::Digest.to_string(), "Digest");
assert_eq!(AuthScheme::Custom("NTLM".to_owned()).to_string(), "NTLM");
}
#[test]
fn auth_scheme_from_str_known() {
assert_eq!("Bearer".parse::<AuthScheme>().unwrap(), AuthScheme::Bearer);
assert_eq!("bearer".parse::<AuthScheme>().unwrap(), AuthScheme::Bearer);
assert_eq!("Basic".parse::<AuthScheme>().unwrap(), AuthScheme::Basic);
assert_eq!("Digest".parse::<AuthScheme>().unwrap(), AuthScheme::Digest);
}
#[test]
fn auth_scheme_from_str_custom() {
let s = "NTLM".parse::<AuthScheme>().unwrap();
assert_eq!(s, AuthScheme::Custom("NTLM".to_owned()));
}
#[test]
fn bearer_token_as_str() {
let t = BearerToken::new("tok");
assert_eq!(t.as_str(), "tok");
}
#[test]
fn bearer_token_debug_redacts() {
let t = BearerToken::new("super-secret");
let dbg = format!("{t:?}");
assert!(dbg.contains("[REDACTED]"));
assert!(!dbg.contains("super-secret"));
}
#[test]
fn bearer_token_display() {
let t = BearerToken::new("tok");
assert_eq!(t.to_string(), "tok");
}
#[test]
fn basic_credentials_fields() {
let c = BasicCredentials::new("alice", "s3cr3t");
assert_eq!(c.username(), "alice");
assert_eq!(c.password(), "s3cr3t");
}
#[test]
fn basic_credentials_debug_redacts_password() {
let c = BasicCredentials::new("alice", "s3cr3t");
let dbg = format!("{c:?}");
assert!(dbg.contains("alice"));
assert!(dbg.contains("[REDACTED]"));
assert!(!dbg.contains("s3cr3t"));
}
#[test]
fn api_key_as_str() {
let k = ApiKeyCredentials::new("key-123");
assert_eq!(k.as_str(), "key-123");
}
#[test]
fn api_key_debug_redacts() {
let k = ApiKeyCredentials::new("key-123");
let dbg = format!("{k:?}");
assert!(dbg.contains("[REDACTED]"));
assert!(!dbg.contains("key-123"));
}
#[test]
fn oauth2_token_fields() {
let t = OAuth2Token::new("access", Some("Bearer"));
assert_eq!(t.as_str(), "access");
assert_eq!(t.token_type(), Some("Bearer"));
}
#[test]
fn oauth2_token_debug_redacts() {
let t = OAuth2Token::new("super-secret-tok", Some("Bearer"));
let dbg = format!("{t:?}");
assert!(dbg.contains("[REDACTED]"));
assert!(!dbg.contains("super-secret-tok"));
}
#[test]
fn parse_bearer() {
let h: AuthorizationHeader = "Bearer mytoken".parse().unwrap();
assert_eq!(h, AuthorizationHeader::Bearer(BearerToken::new("mytoken")));
assert_eq!(h.to_string(), "Bearer mytoken");
}
#[test]
fn parse_basic_roundtrip() {
let h: AuthorizationHeader = "Basic dXNlcjpwYXNz".parse().unwrap();
if let AuthorizationHeader::Basic(c) = &h {
assert_eq!(c.username(), "user");
assert_eq!(c.password(), "pass");
} else {
panic!("expected Basic");
}
assert_eq!(h.to_string(), "Basic dXNlcjpwYXNz");
}
#[test]
fn parse_apikey() {
let h: AuthorizationHeader = "ApiKey abc-def".parse().unwrap();
assert_eq!(
h,
AuthorizationHeader::ApiKey(ApiKeyCredentials::new("abc-def"))
);
assert_eq!(h.to_string(), "ApiKey abc-def");
}
#[test]
fn parse_oauth2() {
let h: AuthorizationHeader = "OAuth2 access123".parse().unwrap();
assert_eq!(h.to_string(), "OAuth2 access123");
}
#[test]
fn parse_other_scheme() {
let h: AuthorizationHeader = "NTLM TlRMTVNTUAAB".parse().unwrap();
assert_eq!(h.to_string(), "NTLM TlRMTVNTUAAB");
assert_eq!(h.scheme(), AuthScheme::Custom("NTLM".to_owned()));
}
#[test]
fn parse_empty_is_error() {
assert_eq!(
"".parse::<AuthorizationHeader>(),
Err(ParseAuthorizationError::Empty)
);
}
#[test]
fn parse_missing_credentials_is_error() {
assert_eq!(
"Bearer".parse::<AuthorizationHeader>(),
Err(ParseAuthorizationError::MissingCredentials)
);
}
#[test]
fn parse_invalid_base64_is_error() {
assert!(matches!(
"Basic !!!".parse::<AuthorizationHeader>(),
Err(ParseAuthorizationError::InvalidBase64(_))
));
}
#[test]
fn parse_basic_missing_colon_is_error() {
use base64::Engine as _;
let encoded = base64::engine::general_purpose::STANDARD.encode(b"userpass");
let input = format!("Basic {encoded}");
assert_eq!(
input.parse::<AuthorizationHeader>(),
Err(ParseAuthorizationError::InvalidBasicFormat)
);
}
#[test]
fn permission_valid() {
let p = Permission::new("orders:read").unwrap();
assert_eq!(p.as_str(), "orders:read");
}
#[test]
fn permission_empty_is_error() {
assert_eq!(Permission::new(""), Err(ParsePermissionError::Empty));
}
#[test]
fn permission_whitespace_is_error() {
assert_eq!(
Permission::new("bad token"),
Err(ParsePermissionError::ContainsWhitespace)
);
}
#[test]
fn permission_display_and_from_str_roundtrip() {
let p = Permission::new("admin").unwrap();
let s = p.to_string();
let back: Permission = s.parse().unwrap();
assert_eq!(back, p);
}
#[test]
fn scope_parse_and_display() {
let s: Scope = "read write openid".parse().unwrap();
assert_eq!(s.len(), 3);
assert!(s.contains("read"));
assert!(s.contains("write"));
assert!(s.contains("openid"));
}
#[test]
fn scope_empty() {
let s: Scope = "".parse().unwrap();
assert!(s.is_empty());
}
#[test]
fn scope_deduplicates() {
let s: Scope = "read read write".parse().unwrap();
assert_eq!(s.len(), 2);
}
#[test]
fn scope_is_subset_of() {
let full: Scope = "read write admin".parse().unwrap();
let partial: Scope = "read write".parse().unwrap();
assert!(partial.is_subset_of(&full));
assert!(!full.is_subset_of(&partial));
}
#[test]
fn scope_self_is_subset_of_self() {
let s: Scope = "read write".parse().unwrap();
assert!(s.is_subset_of(&s));
}
#[test]
fn scope_union() {
let a: Scope = "read".parse().unwrap();
let b: Scope = "write".parse().unwrap();
let c = a.union(&b);
assert!(c.contains("read"));
assert!(c.contains("write"));
assert_eq!(c.len(), 2);
}
#[test]
fn scope_intersection() {
let a: Scope = "read write".parse().unwrap();
let b: Scope = "write admin".parse().unwrap();
let c = a.intersection(&b);
assert_eq!(c.len(), 1);
assert!(c.contains("write"));
assert!(!c.contains("read"));
}
#[test]
fn scope_display_is_sorted() {
let s: Scope = "z a m".parse().unwrap();
assert_eq!(s.to_string(), "a m z");
}
#[cfg(feature = "serde")]
#[test]
fn scope_serde_roundtrip() {
let s: Scope = "read write".parse().unwrap();
let json = serde_json::to_string(&s).unwrap();
let back: Scope = serde_json::from_str(&json).unwrap();
assert_eq!(back, s);
}
#[cfg(feature = "serde")]
#[test]
fn bearer_token_serde_roundtrip() {
let t = BearerToken::new("tok123");
let json = serde_json::to_string(&t).unwrap();
let back: BearerToken = serde_json::from_str(&json).unwrap();
assert_eq!(back, t);
}
#[test]
fn scope_from_permissions() {
let perms = vec![
Permission::new("read").unwrap(),
Permission::new("write").unwrap(),
];
let s = Scope::from_permissions(perms);
assert_eq!(s.len(), 2);
assert!(s.contains("read"));
assert!(s.contains("write"));
}
#[test]
fn scope_empty_constructor() {
let s = Scope::empty();
assert!(s.is_empty());
assert_eq!(s.len(), 0);
}
#[test]
fn scope_iter() {
let s: Scope = "a b c".parse().unwrap();
let tokens: Vec<&str> = s.iter().map(Permission::as_str).collect();
assert_eq!(tokens, vec!["a", "b", "c"]);
}
#[test]
fn scope_try_from_string() {
let s = Scope::try_from("read write".to_owned()).unwrap();
assert!(s.contains("read"));
}
#[test]
fn oauth2_token_no_token_type() {
let t = OAuth2Token::new("tok", None::<String>);
assert_eq!(t.token_type(), None);
assert_eq!(t.as_str(), "tok");
}
#[test]
fn bearer_token_eq() {
let a = BearerToken::new("tok");
let b = BearerToken::new("tok");
let c = BearerToken::new("other");
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn basic_credentials_eq() {
let a = BasicCredentials::new("user", "pass");
let b = BasicCredentials::new("user", "pass");
let c = BasicCredentials::new("user", "wrong");
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn api_key_eq() {
let a = ApiKeyCredentials::new("key");
let b = ApiKeyCredentials::new("key");
let c = ApiKeyCredentials::new("other");
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn oauth2_token_eq() {
let a = OAuth2Token::new("tok", Some("Bearer"));
let b = OAuth2Token::new("tok", Some("Bearer"));
let c = OAuth2Token::new("tok", None::<String>);
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn authorization_header_scheme_all_variants() {
let h: AuthorizationHeader = "Bearer tok".parse().unwrap();
assert_eq!(h.scheme(), AuthScheme::Bearer);
let h: AuthorizationHeader = "Basic dXNlcjpwYXNz".parse().unwrap();
assert_eq!(h.scheme(), AuthScheme::Basic);
let h: AuthorizationHeader = "ApiKey key".parse().unwrap();
assert_eq!(h.scheme(), AuthScheme::ApiKey);
let h: AuthorizationHeader = "OAuth2 tok".parse().unwrap();
assert_eq!(h.scheme(), AuthScheme::OAuth2);
}
#[test]
fn parse_authorization_error_display() {
assert!(!ParseAuthorizationError::Empty.to_string().is_empty());
assert!(
!ParseAuthorizationError::MissingCredentials
.to_string()
.is_empty()
);
assert!(
!ParseAuthorizationError::InvalidBase64("bad".to_owned())
.to_string()
.is_empty()
);
assert!(
!ParseAuthorizationError::InvalidBasicFormat
.to_string()
.is_empty()
);
assert!(!ParseAuthorizationError::InvalidUtf8.to_string().is_empty());
}
#[test]
fn parse_basic_invalid_utf8_is_error() {
use base64::Engine as _;
let encoded = base64::engine::general_purpose::STANDARD.encode(b"\xFF\xFE");
let input = format!("Basic {encoded}");
assert_eq!(
input.parse::<AuthorizationHeader>(),
Err(ParseAuthorizationError::InvalidUtf8)
);
}
#[test]
fn parse_permission_error_display() {
assert!(!ParsePermissionError::Empty.to_string().is_empty());
assert!(
!ParsePermissionError::ContainsWhitespace
.to_string()
.is_empty()
);
}
#[test]
fn parse_scope_error_display() {
let err = ParseScopeError::from(ParsePermissionError::Empty);
assert!(!err.to_string().is_empty());
}
}