use std::collections::HashSet;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("Invalid pattern: {0}")]
InvalidPattern(String),
#[error("Unknown route path: {0}. Valid paths are: /v1/mint/quote/{{method}}, /v1/mint/{{method}}, /v1/melt/quote/{{method}}, /v1/melt/{{method}}, /v1/swap, /v1/checkstate, /v1/restore, /v1/auth/blind/mint, /v1/ws")]
UnknownRoute(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize)]
pub struct Settings {
pub openid_discovery: String,
pub client_id: String,
pub protected_endpoints: Vec<ProtectedEndpoint>,
}
impl Settings {
pub fn new(
openid_discovery: String,
client_id: String,
protected_endpoints: Vec<ProtectedEndpoint>,
) -> Self {
Self {
openid_discovery,
client_id,
protected_endpoints,
}
}
}
impl<'de> Deserialize<'de> for Settings {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct RawSettings {
openid_discovery: String,
client_id: String,
protected_endpoints: Vec<RawProtectedEndpoint>,
}
#[derive(Deserialize)]
struct RawProtectedEndpoint {
method: Method,
path: String,
}
let raw = RawSettings::deserialize(deserializer)?;
let mut protected_endpoints = HashSet::new();
for raw_endpoint in raw.protected_endpoints {
let expanded_paths = matching_route_paths(&raw_endpoint.path).map_err(|e| {
serde::de::Error::custom(format!("Invalid pattern '{}': {}", raw_endpoint.path, e))
})?;
for path in expanded_paths {
protected_endpoints.insert(ProtectedEndpoint::new(raw_endpoint.method, path));
}
}
Ok(Settings {
openid_discovery: raw.openid_discovery,
client_id: raw.client_id,
protected_endpoints: protected_endpoints.into_iter().collect(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ProtectedEndpoint {
pub method: Method,
pub path: RoutePath,
}
impl ProtectedEndpoint {
pub fn new(method: Method, path: RoutePath) -> Self {
Self { method, path }
}
pub fn match_specificity(&self, endpoint: &Self) -> Option<usize> {
if self.method != endpoint.method {
return None;
}
self.path.match_specificity(&endpoint.path)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum Method {
Get,
Post,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RoutePath {
Wildcard(String),
MintQuote(String),
Mint(String),
MeltQuote(String),
Melt(String),
Swap,
Checkstate,
Restore,
MintBlindAuth,
Ws,
}
impl Serialize for RoutePath {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl std::str::FromStr for RoutePath {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(prefix) = s.strip_suffix('*') {
return Self::wildcard(prefix.to_string());
}
match s {
"/v1/swap" => Ok(RoutePath::Swap),
"/v1/checkstate" => Ok(RoutePath::Checkstate),
"/v1/restore" => Ok(RoutePath::Restore),
"/v1/auth/blind/mint" => Ok(RoutePath::MintBlindAuth),
"/v1/ws" => Ok(RoutePath::Ws),
_ => {
if let Some(method) = s.strip_prefix("/v1/mint/quote/") {
Ok(RoutePath::MintQuote(normalize_payment_method(method)))
} else if let Some(method) = s.strip_prefix("/v1/mint/") {
Ok(RoutePath::Mint(normalize_payment_method(method)))
} else if let Some(method) = s.strip_prefix("/v1/melt/quote/") {
Ok(RoutePath::MeltQuote(normalize_payment_method(method)))
} else if let Some(method) = s.strip_prefix("/v1/melt/") {
Ok(RoutePath::Melt(normalize_payment_method(method)))
} else {
Err(Error::UnknownRoute(s.to_string()))
}
}
}
}
}
impl<'de> Deserialize<'de> for RoutePath {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
RoutePath::from_str(&s).map_err(serde::de::Error::custom)
}
}
impl RoutePath {
pub fn wildcard(prefix: String) -> Result<Self, Error> {
if prefix.contains('*') {
return Err(Error::InvalidPattern(
"Wildcard '*' must be the last character".to_string(),
));
}
Ok(Self::Wildcard(prefix))
}
pub fn is_wildcard(&self) -> bool {
matches!(self, Self::Wildcard(_))
}
pub fn match_specificity(&self, endpoint: &Self) -> Option<usize> {
match self {
Self::Wildcard(prefix) if endpoint.to_string().starts_with(prefix) => {
Some(prefix.len())
}
Self::MintQuote(method) => match endpoint {
Self::MintQuote(endpoint_method)
if payment_methods_match(method, endpoint_method) =>
{
Some(usize::MAX)
}
_ => None,
},
Self::Mint(method) => match endpoint {
Self::Mint(endpoint_method) if payment_methods_match(method, endpoint_method) => {
Some(usize::MAX)
}
_ => None,
},
Self::MeltQuote(method) => match endpoint {
Self::MeltQuote(endpoint_method)
if payment_methods_match(method, endpoint_method) =>
{
Some(usize::MAX)
}
_ => None,
},
Self::Melt(method) => match endpoint {
Self::Melt(endpoint_method) if payment_methods_match(method, endpoint_method) => {
Some(usize::MAX)
}
_ => None,
},
_ if self == endpoint => Some(usize::MAX),
_ => None,
}
}
pub fn static_paths() -> Vec<RoutePath> {
vec![
RoutePath::Swap,
RoutePath::Checkstate,
RoutePath::Restore,
RoutePath::MintBlindAuth,
RoutePath::Ws,
]
}
pub fn common_payment_method_paths() -> Vec<RoutePath> {
let methods = vec!["bolt11", "bolt12"];
let mut paths = Vec::new();
for method in methods {
paths.push(RoutePath::MintQuote(method.to_string()));
paths.push(RoutePath::Mint(method.to_string()));
paths.push(RoutePath::MeltQuote(method.to_string()));
paths.push(RoutePath::Melt(method.to_string()));
}
paths
}
pub fn all_known_paths() -> Vec<RoutePath> {
let mut paths = Self::static_paths();
paths.extend(Self::common_payment_method_paths());
paths
}
}
fn normalize_payment_method(method: &str) -> String {
method.to_lowercase()
}
fn payment_methods_match(configured: &str, endpoint: &str) -> bool {
configured.to_lowercase() == endpoint.to_lowercase()
}
pub fn matching_route_paths(pattern: &str) -> Result<Vec<RoutePath>, Error> {
if let Some(prefix) = pattern.strip_suffix('*') {
let wildcard = RoutePath::wildcard(prefix.to_string())?;
let mut paths: Vec<RoutePath> = RoutePath::all_known_paths()
.into_iter()
.filter(|path| path.to_string().starts_with(prefix))
.collect();
paths.push(wildcard);
Ok(paths)
} else {
if pattern.contains('*') {
return Err(Error::InvalidPattern(
"Wildcard '*' must be the last character".to_string(),
));
}
match RoutePath::from_str(pattern) {
Ok(path) => Ok(vec![path]),
Err(e) => Err(e),
}
}
}
impl std::fmt::Display for RoutePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RoutePath::Wildcard(prefix) => write!(f, "{}*", prefix),
RoutePath::MintQuote(method) => write!(f, "/v1/mint/quote/{}", method),
RoutePath::Mint(method) => write!(f, "/v1/mint/{}", method),
RoutePath::MeltQuote(method) => write!(f, "/v1/melt/quote/{}", method),
RoutePath::Melt(method) => write!(f, "/v1/melt/{}", method),
RoutePath::Swap => write!(f, "/v1/swap"),
RoutePath::Checkstate => write!(f, "/v1/checkstate"),
RoutePath::Restore => write!(f, "/v1/restore"),
RoutePath::MintBlindAuth => write!(f, "/v1/auth/blind/mint"),
RoutePath::Ws => write!(f, "/v1/ws"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nut00::KnownMethod;
use crate::PaymentMethod;
#[test]
fn test_matching_route_paths_root_wildcard() {
let paths = matching_route_paths("*").unwrap();
assert_eq!(paths.len(), RoutePath::all_known_paths().len() + 1);
assert!(paths.contains(&RoutePath::Wildcard(String::new())));
}
#[test]
fn test_matching_route_paths_middle_wildcard() {
let result = matching_route_paths("/v1/*/mint");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::InvalidPattern(_)));
}
#[test]
fn test_matching_route_paths_prefix_without_slash() {
let paths = matching_route_paths("/v1/mint*").unwrap();
assert_eq!(paths.len(), 5);
assert!(paths.contains(&RoutePath::Wildcard("/v1/mint".to_string())));
assert!(!paths.contains(&RoutePath::MeltQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
}
#[test]
fn test_matching_route_paths_exact_match_unknown() {
let result = matching_route_paths("/v1/invalid/path");
assert!(matches!(
result,
Err(Error::UnknownRoute(route)) if route == "/v1/invalid/path"
));
}
#[test]
fn test_matching_route_paths_dynamic_method() {
let paths = matching_route_paths("/v1/mint/custom_method").unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0], RoutePath::Mint("custom_method".to_string()));
}
#[test]
fn test_matching_route_paths_all() {
let paths = matching_route_paths("/v1/*").unwrap();
assert_eq!(paths.len(), RoutePath::all_known_paths().len() + 1);
assert!(paths.contains(&RoutePath::Wildcard("/v1/".to_string())));
assert!(paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(paths.contains(&RoutePath::Mint(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(paths.contains(&RoutePath::MeltQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(paths.contains(&RoutePath::Melt(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt12).to_string()
)));
assert!(paths.contains(&RoutePath::Mint(
PaymentMethod::Known(KnownMethod::Bolt12).to_string()
)));
assert!(paths.contains(&RoutePath::Swap));
assert!(paths.contains(&RoutePath::Checkstate));
assert!(paths.contains(&RoutePath::Restore));
assert!(paths.contains(&RoutePath::MintBlindAuth));
}
#[test]
fn test_matching_route_paths_mint_only() {
let paths = matching_route_paths("/v1/mint/*").unwrap();
assert_eq!(paths.len(), 5);
assert!(paths.contains(&RoutePath::Wildcard("/v1/mint/".to_string())));
assert!(paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(paths.contains(&RoutePath::Mint(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt12).to_string()
)));
assert!(paths.contains(&RoutePath::Mint(
PaymentMethod::Known(KnownMethod::Bolt12).to_string()
)));
assert!(!paths.contains(&RoutePath::MeltQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(!paths.contains(&RoutePath::Melt(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(!paths.contains(&RoutePath::MeltQuote(
PaymentMethod::Known(KnownMethod::Bolt12).to_string()
)));
assert!(!paths.contains(&RoutePath::Melt(
PaymentMethod::Known(KnownMethod::Bolt12).to_string()
)));
assert!(!paths.contains(&RoutePath::Swap));
}
#[test]
fn test_matching_route_paths_quote_only() {
let paths = matching_route_paths("/v1/mint/quote/*").unwrap();
assert_eq!(paths.len(), 3);
assert!(paths.contains(&RoutePath::Wildcard("/v1/mint/quote/".to_string())));
assert!(paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt12).to_string()
)));
assert!(!paths.contains(&RoutePath::Mint(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(!paths.contains(&RoutePath::Melt(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
}
#[test]
fn test_matching_route_paths_no_match() {
let result = matching_route_paths("/nonexistent/path");
assert!(matches!(
result,
Err(Error::UnknownRoute(route)) if route == "/nonexistent/path"
));
}
#[test]
fn test_matching_route_paths_quote_bolt11_only() {
let paths = matching_route_paths("/v1/mint/quote/bolt11").unwrap();
assert_eq!(paths.len(), 1);
assert!(paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
}
#[test]
fn test_matching_route_paths_invalid_regex() {
let result = matching_route_paths("/*unclosed parenthesis");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::InvalidPattern(_)));
}
#[test]
fn test_route_path_to_string() {
assert_eq!(
RoutePath::Wildcard("/v1/mint/".to_string()).to_string(),
"/v1/mint/*"
);
assert_eq!(
RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()).to_string(),
"/v1/mint/quote/bolt11"
);
assert_eq!(
RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string()).to_string(),
"/v1/mint/bolt11"
);
assert_eq!(
RoutePath::MeltQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()).to_string(),
"/v1/melt/quote/bolt11"
);
assert_eq!(
RoutePath::Melt(PaymentMethod::Known(KnownMethod::Bolt11).to_string()).to_string(),
"/v1/melt/bolt11"
);
assert_eq!(
RoutePath::MintQuote("paypal".to_string()).to_string(),
"/v1/mint/quote/paypal"
);
assert_eq!(RoutePath::Swap.to_string(), "/v1/swap");
assert_eq!(RoutePath::Checkstate.to_string(), "/v1/checkstate");
assert_eq!(RoutePath::Restore.to_string(), "/v1/restore");
assert_eq!(RoutePath::MintBlindAuth.to_string(), "/v1/auth/blind/mint");
}
#[test]
fn test_route_path_is_wildcard() {
assert!(RoutePath::Wildcard("/v1/mint/".to_string()).is_wildcard());
assert!(!RoutePath::MintQuote("bolt11".to_string()).is_wildcard());
assert!(!RoutePath::Swap.is_wildcard());
}
#[test]
fn test_protected_endpoint_match_specificity() {
let request = ProtectedEndpoint::new(
Method::Post,
RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
);
let exact = ProtectedEndpoint::new(
Method::Post,
RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
);
let wildcard =
ProtectedEndpoint::new(Method::Post, RoutePath::Wildcard("/v1/mint/".to_string()));
let different_method = ProtectedEndpoint::new(
Method::Get,
RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
);
let non_matching_path = ProtectedEndpoint::new(Method::Post, RoutePath::Swap);
assert_eq!(exact.match_specificity(&request), Some(usize::MAX));
assert_eq!(
wildcard.match_specificity(&request),
Some("/v1/mint/".len())
);
assert_eq!(different_method.match_specificity(&request), None);
assert_eq!(non_matching_path.match_specificity(&request), None);
}
#[test]
fn test_payment_method_routes_match_case_insensitively() {
let cases = [
(
RoutePath::MintQuote("bolt11".to_string()),
RoutePath::MintQuote("BOLT11".to_string()),
),
(
RoutePath::Mint("paypal".to_string()),
RoutePath::Mint("PayPal".to_string()),
),
(
RoutePath::MeltQuote("bolt12".to_string()),
RoutePath::MeltQuote("BOLT12".to_string()),
),
(
RoutePath::Melt("custom".to_string()),
RoutePath::Melt("CUSTOM".to_string()),
),
];
for (configured, request) in cases {
assert_eq!(configured.match_specificity(&request), Some(usize::MAX));
}
}
#[test]
fn test_protected_endpoint_matches_case_insensitive_payment_method() {
let configured =
ProtectedEndpoint::new(Method::Post, RoutePath::MintQuote("bolt11".to_string()));
let request =
ProtectedEndpoint::new(Method::Post, RoutePath::MintQuote("BOLT11".to_string()));
assert_eq!(configured.match_specificity(&request), Some(usize::MAX));
}
#[test]
fn test_route_path_serialization() {
let json = serde_json::to_string(&RoutePath::Mint(
PaymentMethod::Known(KnownMethod::Bolt11).to_string(),
))
.unwrap();
assert_eq!(json, "\"/v1/mint/bolt11\"");
let json = serde_json::to_string(&RoutePath::MintQuote("paypal".to_string())).unwrap();
assert_eq!(json, "\"/v1/mint/quote/paypal\"");
let path: RoutePath = serde_json::from_str("\"/v1/mint/bolt11\"").unwrap();
assert_eq!(
path,
RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string())
);
let path: RoutePath = serde_json::from_str("\"/v1/mint/quote/BOLT11\"").unwrap();
assert_eq!(path, RoutePath::MintQuote("bolt11".to_string()));
let path: RoutePath = serde_json::from_str("\"/v1/melt/quote/venmo\"").unwrap();
assert_eq!(path, RoutePath::MeltQuote("venmo".to_string()));
let original = RoutePath::Melt(PaymentMethod::Known(KnownMethod::Bolt12).to_string());
let json = serde_json::to_string(&original).unwrap();
let deserialized: RoutePath = serde_json::from_str(&json).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn test_settings_deserialize_direct_paths() {
let json = r#"{
"openid_discovery": "https://example.com/.well-known/openid-configuration",
"client_id": "client123",
"protected_endpoints": [
{
"method": "GET",
"path": "/v1/mint/bolt11"
},
{
"method": "POST",
"path": "/v1/swap"
}
]
}"#;
let settings: Settings = serde_json::from_str(json).unwrap();
assert_eq!(
settings.openid_discovery,
"https://example.com/.well-known/openid-configuration"
);
assert_eq!(settings.client_id, "client123");
assert_eq!(settings.protected_endpoints.len(), 2);
let paths = settings
.protected_endpoints
.iter()
.map(|ep| (ep.method, ep.path.clone()))
.collect::<Vec<_>>();
assert!(paths.contains(&(
Method::Get,
RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string())
)));
assert!(paths.contains(&(Method::Post, RoutePath::Swap)));
}
#[test]
fn test_settings_deserialize_with_regex() {
let json = r#"{
"openid_discovery": "https://example.com/.well-known/openid-configuration",
"client_id": "client123",
"protected_endpoints": [
{
"method": "GET",
"path": "/v1/mint/*"
},
{
"method": "POST",
"path": "/v1/swap"
}
]
}"#;
let settings: Settings = serde_json::from_str(json).unwrap();
assert_eq!(
settings.openid_discovery,
"https://example.com/.well-known/openid-configuration"
);
assert_eq!(settings.client_id, "client123");
assert_eq!(settings.protected_endpoints.len(), 6);
let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
ProtectedEndpoint::new(
Method::Get,
RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
),
ProtectedEndpoint::new(
Method::Get,
RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
),
ProtectedEndpoint::new(
Method::Get,
RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt12).to_string()),
),
ProtectedEndpoint::new(
Method::Get,
RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt12).to_string()),
),
ProtectedEndpoint::new(Method::Get, RoutePath::Wildcard("/v1/mint/".to_string())),
]);
let deserlized_protected = settings.protected_endpoints.into_iter().collect();
assert_eq!(expected_protected, deserlized_protected);
}
#[test]
fn test_settings_deserialize_invalid_regex() {
let json = r#"{
"openid_discovery": "https://example.com/.well-known/openid-configuration",
"client_id": "client123",
"protected_endpoints": [
{
"method": "GET",
"path": "/*wildcard_start"
}
]
}"#;
let result = serde_json::from_str::<Settings>(json);
assert!(result.is_err());
}
#[test]
fn test_settings_deserialize_unknown_exact_path() {
let json = r#"{
"openid_discovery": "https://example.com/.well-known/openid-configuration",
"client_id": "client123",
"protected_endpoints": [
{
"method": "POST",
"path": "/v1/swp"
}
]
}"#;
let result = serde_json::from_str::<Settings>(json);
assert!(result.is_err());
}
#[test]
fn test_settings_deserialize_exact_path_match() {
let json = r#"{
"openid_discovery": "https://example.com/.well-known/openid-configuration",
"client_id": "client123",
"protected_endpoints": [
{
"method": "GET",
"path": "/v1/mint/quote/bolt11"
}
]
}"#;
let settings: Settings = serde_json::from_str(json).unwrap();
assert_eq!(settings.protected_endpoints.len(), 1);
assert_eq!(settings.protected_endpoints[0].method, Method::Get);
assert_eq!(
settings.protected_endpoints[0].path,
RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string())
);
}
#[test]
fn test_settings_deserialize_all_paths() {
let json = r#"{
"openid_discovery": "https://example.com/.well-known/openid-configuration",
"client_id": "client123",
"protected_endpoints": [
{
"method": "GET",
"path": "/v1/*"
}
]
}"#;
let settings: Settings = serde_json::from_str(json).unwrap();
assert_eq!(
settings.protected_endpoints.len(),
RoutePath::all_known_paths().len() + 1
);
}
#[test]
fn test_matching_route_paths_empty_pattern() {
let result = matching_route_paths("");
assert!(matches!(result, Err(Error::UnknownRoute(route)) if route.is_empty()));
}
#[test]
fn test_matching_route_paths_just_slash() {
let result = matching_route_paths("/");
assert!(matches!(result, Err(Error::UnknownRoute(route)) if route == "/"));
}
#[test]
fn test_matching_route_paths_trailing_slash() {
let result = matching_route_paths("/v1/mint/*/");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::InvalidPattern(_)));
}
#[test]
fn test_matching_route_paths_consecutive_wildcards() {
let result = matching_route_paths("**");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::InvalidPattern(_)));
}
#[test]
fn test_matching_route_paths_method_specific() {
let paths = matching_route_paths("/v1/swap").unwrap();
assert_eq!(paths.len(), 1);
assert!(paths.contains(&RoutePath::Swap));
}
#[test]
fn test_settings_mixed_methods() {
let json = r#"{
"openid_discovery": "https://example.com/.well-known/openid-configuration",
"client_id": "client123",
"protected_endpoints": [
{
"method": "GET",
"path": "/v1/swap"
},
{
"method": "POST",
"path": "/v1/swap"
}
]
}"#;
let settings: Settings = serde_json::from_str(json).unwrap();
assert_eq!(settings.protected_endpoints.len(), 2);
let methods: Vec<_> = settings
.protected_endpoints
.iter()
.map(|ep| ep.method)
.collect();
assert!(methods.contains(&Method::Get));
assert!(methods.contains(&Method::Post));
for ep in &settings.protected_endpoints {
assert_eq!(ep.path, RoutePath::Swap);
}
}
#[test]
fn test_matching_route_paths_melt_prefix() {
let paths = matching_route_paths("/v1/melt/*").unwrap();
assert_eq!(paths.len(), 5);
assert!(paths.contains(&RoutePath::Wildcard("/v1/melt/".to_string())));
assert!(paths.contains(&RoutePath::Melt(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(paths.contains(&RoutePath::MeltQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(paths.contains(&RoutePath::Melt(
PaymentMethod::Known(KnownMethod::Bolt12).to_string()
)));
assert!(paths.contains(&RoutePath::MeltQuote(
PaymentMethod::Known(KnownMethod::Bolt12).to_string()
)));
assert!(!paths.contains(&RoutePath::Mint(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
}
#[test]
fn test_matching_route_paths_static_exact() {
let swap_paths = matching_route_paths("/v1/swap").unwrap();
assert_eq!(swap_paths.len(), 1);
assert_eq!(swap_paths[0], RoutePath::Swap);
let checkstate_paths = matching_route_paths("/v1/checkstate").unwrap();
assert_eq!(checkstate_paths.len(), 1);
assert_eq!(checkstate_paths[0], RoutePath::Checkstate);
let restore_paths = matching_route_paths("/v1/restore").unwrap();
assert_eq!(restore_paths.len(), 1);
assert_eq!(restore_paths[0], RoutePath::Restore);
let ws_paths = matching_route_paths("/v1/ws").unwrap();
assert_eq!(ws_paths.len(), 1);
assert_eq!(ws_paths[0], RoutePath::Ws);
}
#[test]
fn test_matching_route_paths_auth_blind_mint() {
let paths = matching_route_paths("/v1/auth/blind/mint").unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0], RoutePath::MintBlindAuth);
}
#[test]
fn test_settings_empty_endpoints() {
let json = r#"{
"openid_discovery": "https://example.com/.well-known/openid-configuration",
"client_id": "client123",
"protected_endpoints": []
}"#;
let settings: Settings = serde_json::from_str(json).unwrap();
assert!(settings.protected_endpoints.is_empty());
}
#[test]
fn test_settings_duplicate_paths() {
let json = r#"{
"openid_discovery": "https://example.com/.well-known/openid-configuration",
"client_id": "client123",
"protected_endpoints": [
{
"method": "POST",
"path": "/v1/swap"
},
{
"method": "POST",
"path": "/v1/swap"
}
]
}"#;
let settings: Settings = serde_json::from_str(json).unwrap();
assert_eq!(settings.protected_endpoints.len(), 1);
assert_eq!(settings.protected_endpoints[0].method, Method::Post);
assert_eq!(settings.protected_endpoints[0].path, RoutePath::Swap);
}
#[test]
fn test_matching_route_paths_only_wildcard() {
let paths = matching_route_paths("*").unwrap();
assert_eq!(paths.len(), RoutePath::all_known_paths().len() + 1);
assert!(paths.contains(&RoutePath::Wildcard(String::new())));
}
#[test]
fn test_matching_route_paths_wildcard_in_middle() {
let result = matching_route_paths("/v1/*/bolt11");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), Error::InvalidPattern(_)));
}
#[test]
fn test_exact_match_no_child_paths() {
let result = matching_route_paths("/v1/mint");
assert!(matches!(
result,
Err(Error::UnknownRoute(route)) if route == "/v1/mint"
));
}
#[test]
fn test_exact_match_no_extra_path() {
let paths = matching_route_paths("/v1/swap").unwrap();
assert_eq!(paths.len(), 1);
assert_eq!(paths[0], RoutePath::Swap);
assert!(!paths.contains(&RoutePath::Checkstate));
assert!(!paths.contains(&RoutePath::Restore));
}
#[test]
fn test_partial_prefix_matching() {
let paths = matching_route_paths("/v1/mi*").unwrap();
assert!(paths.contains(&RoutePath::Mint(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(!paths.contains(&RoutePath::Melt(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(!paths.contains(&RoutePath::MeltQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
}
#[test]
fn test_exact_match_wrong_payment_method() {
let paths = matching_route_paths("/v1/mint/quote/bolt11").unwrap();
assert_eq!(paths.len(), 1);
assert!(paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(!paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt12).to_string()
)));
assert!(!paths.contains(&RoutePath::Mint(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
}
#[test]
fn test_prefix_match_wrong_category() {
let paths = matching_route_paths("/v1/mint/*").unwrap();
assert!(paths.contains(&RoutePath::Mint(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(!paths.contains(&RoutePath::Melt(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(!paths.contains(&RoutePath::MeltQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(!paths.contains(&RoutePath::Swap));
assert!(!paths.contains(&RoutePath::Checkstate));
}
#[test]
fn test_case_sensitivity() {
let paths_upper = matching_route_paths("/v1/MINT/*").unwrap();
let paths_lower = matching_route_paths("/v1/mint/*").unwrap();
assert_eq!(
paths_upper,
vec![RoutePath::Wildcard("/v1/MINT/".to_string())]
);
assert_eq!(paths_lower.len(), 5);
}
#[test]
fn test_negative_assertions_comprehensive() {
let bolt11_paths = matching_route_paths("/v1/mint/quote/bolt11").unwrap();
assert!(!bolt11_paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt12).to_string()
)));
let mint_paths = matching_route_paths("/v1/mint/*").unwrap();
assert!(!mint_paths.contains(&RoutePath::Melt(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(!mint_paths.contains(&RoutePath::MeltQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
let swap_paths = matching_route_paths("/v1/swap").unwrap();
assert!(!swap_paths.contains(&RoutePath::Checkstate));
assert!(!swap_paths.contains(&RoutePath::Restore));
assert!(!swap_paths.contains(&RoutePath::MintBlindAuth));
assert!(matches!(
matching_route_paths("/V1/SWAP"),
Err(Error::UnknownRoute(route)) if route == "/V1/SWAP"
));
assert_eq!(
matching_route_paths("/V1/MINT/*").unwrap(),
vec![RoutePath::Wildcard("/V1/MINT/".to_string())]
);
assert!(matches!(
matching_route_paths("/unknown/path"),
Err(Error::UnknownRoute(route)) if route == "/unknown/path"
));
assert!(matches!(
matching_route_paths("/invalid"),
Err(Error::UnknownRoute(route)) if route == "/invalid"
));
}
#[test]
fn test_prefix_vs_exact_boundary() {
let paths = matching_route_paths("/v1/mint/quote/*").unwrap();
assert!(!paths.is_empty());
assert!(paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt11).to_string()
)));
assert!(paths.contains(&RoutePath::MintQuote(
PaymentMethod::Known(KnownMethod::Bolt12).to_string()
)));
assert_eq!(paths.len(), 3);
assert!(paths.contains(&RoutePath::Wildcard("/v1/mint/quote/".to_string())));
}
#[test]
fn test_wildcard_protects_custom_payment_methods() {
let paths_star = matching_route_paths("/v1/*").unwrap();
let paths_mint = matching_route_paths("/v1/mint/*").unwrap();
let custom_mint = RoutePath::Mint("paypal".to_string());
let custom_mint_quote = RoutePath::MintQuote("paypal".to_string());
assert!(paths_star
.iter()
.any(|path| path.match_specificity(&custom_mint).is_some()));
assert!(paths_star
.iter()
.any(|path| path.match_specificity(&custom_mint_quote).is_some()));
assert!(paths_mint
.iter()
.any(|path| path.match_specificity(&custom_mint).is_some()));
assert!(paths_mint
.iter()
.any(|path| path.match_specificity(&custom_mint_quote).is_some()));
}
}