1use std::collections::HashSet;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7use thiserror::Error;
8
9#[derive(Debug, Error)]
11pub enum Error {
12 #[error("Invalid pattern: {0}")]
14 InvalidPattern(String),
15 #[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")]
17 UnknownRoute(String),
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize)]
22#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
23pub struct Settings {
24 pub openid_discovery: String,
26 pub client_id: String,
28 pub protected_endpoints: Vec<ProtectedEndpoint>,
30}
31
32impl Settings {
33 pub fn new(
35 openid_discovery: String,
36 client_id: String,
37 protected_endpoints: Vec<ProtectedEndpoint>,
38 ) -> Self {
39 Self {
40 openid_discovery,
41 client_id,
42 protected_endpoints,
43 }
44 }
45}
46
47impl<'de> Deserialize<'de> for Settings {
49 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
50 where
51 D: serde::Deserializer<'de>,
52 {
53 #[derive(Deserialize)]
55 struct RawSettings {
56 openid_discovery: String,
57 client_id: String,
58 protected_endpoints: Vec<RawProtectedEndpoint>,
59 }
60
61 #[derive(Deserialize)]
62 struct RawProtectedEndpoint {
63 method: Method,
64 path: String,
65 }
66
67 let raw = RawSettings::deserialize(deserializer)?;
69
70 let mut protected_endpoints = HashSet::new();
72
73 for raw_endpoint in raw.protected_endpoints {
74 let expanded_paths = matching_route_paths(&raw_endpoint.path).map_err(|e| {
75 serde::de::Error::custom(format!("Invalid pattern '{}': {}", raw_endpoint.path, e))
76 })?;
77
78 for path in expanded_paths {
79 protected_endpoints.insert(ProtectedEndpoint::new(raw_endpoint.method, path));
80 }
81 }
82
83 Ok(Settings {
85 openid_discovery: raw.openid_discovery,
86 client_id: raw.client_id,
87 protected_endpoints: protected_endpoints.into_iter().collect(),
88 })
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
94#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
95pub struct ProtectedEndpoint {
96 pub method: Method,
98 pub path: RoutePath,
100}
101
102impl ProtectedEndpoint {
103 pub fn new(method: Method, path: RoutePath) -> Self {
105 Self { method, path }
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
111#[serde(rename_all = "UPPERCASE")]
112#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
113pub enum Method {
114 Get,
116 Post,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Hash)]
122#[cfg_attr(feature = "swagger", derive(utoipa::ToSchema))]
123pub enum RoutePath {
124 MintQuote(String),
126 Mint(String),
128 MeltQuote(String),
130 Melt(String),
132 Swap,
134 Checkstate,
136 Restore,
138 MintBlindAuth,
140 Ws,
142}
143
144impl Serialize for RoutePath {
145 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
146 where
147 S: serde::Serializer,
148 {
149 serializer.serialize_str(&self.to_string())
150 }
151}
152
153impl std::str::FromStr for RoutePath {
154 type Err = Error;
155
156 fn from_str(s: &str) -> Result<Self, Self::Err> {
157 match s {
159 "/v1/swap" => Ok(RoutePath::Swap),
160 "/v1/checkstate" => Ok(RoutePath::Checkstate),
161 "/v1/restore" => Ok(RoutePath::Restore),
162 "/v1/auth/blind/mint" => Ok(RoutePath::MintBlindAuth),
163 "/v1/ws" => Ok(RoutePath::Ws),
164 _ => {
165 if let Some(method) = s.strip_prefix("/v1/mint/quote/") {
167 Ok(RoutePath::MintQuote(method.to_string()))
168 } else if let Some(method) = s.strip_prefix("/v1/mint/") {
169 Ok(RoutePath::Mint(method.to_string()))
170 } else if let Some(method) = s.strip_prefix("/v1/melt/quote/") {
171 Ok(RoutePath::MeltQuote(method.to_string()))
172 } else if let Some(method) = s.strip_prefix("/v1/melt/") {
173 Ok(RoutePath::Melt(method.to_string()))
174 } else {
175 Err(Error::UnknownRoute(s.to_string()))
178 }
179 }
180 }
181 }
182}
183
184impl<'de> Deserialize<'de> for RoutePath {
185 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
186 where
187 D: serde::Deserializer<'de>,
188 {
189 let s = String::deserialize(deserializer)?;
190 RoutePath::from_str(&s).map_err(serde::de::Error::custom)
191 }
192}
193
194impl RoutePath {
195 pub fn static_paths() -> Vec<RoutePath> {
198 vec![
199 RoutePath::Swap,
200 RoutePath::Checkstate,
201 RoutePath::Restore,
202 RoutePath::MintBlindAuth,
203 RoutePath::Ws,
204 ]
205 }
206
207 pub fn common_payment_method_paths() -> Vec<RoutePath> {
210 let methods = vec!["bolt11", "bolt12"];
211 let mut paths = Vec::new();
212
213 for method in methods {
214 paths.push(RoutePath::MintQuote(method.to_string()));
215 paths.push(RoutePath::Mint(method.to_string()));
216 paths.push(RoutePath::MeltQuote(method.to_string()));
217 paths.push(RoutePath::Melt(method.to_string()));
218 }
219
220 paths
221 }
222
223 pub fn all_known_paths() -> Vec<RoutePath> {
225 let mut paths = Self::static_paths();
226 paths.extend(Self::common_payment_method_paths());
227 paths
228 }
229}
230
231pub fn matching_route_paths(pattern: &str) -> Result<Vec<RoutePath>, Error> {
233 if let Some(prefix) = pattern.strip_suffix('*') {
235 if prefix.contains('*') {
238 return Err(Error::InvalidPattern(
239 "Wildcard '*' must be the last character".to_string(),
240 ));
241 }
242
243 Ok(RoutePath::all_known_paths()
245 .into_iter()
246 .filter(|path| path.to_string().starts_with(prefix))
247 .collect())
248 } else {
249 if pattern.contains('*') {
251 return Err(Error::InvalidPattern(
252 "Wildcard '*' must be the last character".to_string(),
253 ));
254 }
255
256 match RoutePath::from_str(pattern) {
257 Ok(path) => Ok(vec![path]),
258 Err(_) => Ok(vec![]), }
260 }
261}
262impl std::fmt::Display for RoutePath {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 match self {
265 RoutePath::MintQuote(method) => write!(f, "/v1/mint/quote/{}", method),
266 RoutePath::Mint(method) => write!(f, "/v1/mint/{}", method),
267 RoutePath::MeltQuote(method) => write!(f, "/v1/melt/quote/{}", method),
268 RoutePath::Melt(method) => write!(f, "/v1/melt/{}", method),
269 RoutePath::Swap => write!(f, "/v1/swap"),
270 RoutePath::Checkstate => write!(f, "/v1/checkstate"),
271 RoutePath::Restore => write!(f, "/v1/restore"),
272 RoutePath::MintBlindAuth => write!(f, "/v1/auth/blind/mint"),
273 RoutePath::Ws => write!(f, "/v1/ws"),
274 }
275 }
276}
277
278#[cfg(test)]
279mod tests {
280
281 use super::*;
282 use crate::nut00::KnownMethod;
283 use crate::PaymentMethod;
284
285 #[test]
286 fn test_matching_route_paths_root_wildcard() {
287 let paths = matching_route_paths("*").unwrap();
289
290 assert_eq!(paths.len(), RoutePath::all_known_paths().len());
292 }
293
294 #[test]
295 fn test_matching_route_paths_middle_wildcard() {
296 let result = matching_route_paths("/v1/*/mint");
298 assert!(result.is_err());
299 assert!(matches!(result.unwrap_err(), Error::InvalidPattern(_)));
300 }
301
302 #[test]
303 fn test_matching_route_paths_prefix_without_slash() {
304 let paths = matching_route_paths("/v1/mint*").unwrap();
306
307 assert_eq!(paths.len(), 4);
309
310 assert!(!paths.contains(&RoutePath::MeltQuote(
312 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
313 )));
314 }
315
316 #[test]
317 fn test_matching_route_paths_exact_match_unknown() {
318 let paths = matching_route_paths("/v1/invalid/path").unwrap();
320 assert!(paths.is_empty());
321 }
322
323 #[test]
324 fn test_matching_route_paths_dynamic_method() {
325 let paths = matching_route_paths("/v1/mint/custom_method").unwrap();
327 assert_eq!(paths.len(), 1);
328 assert_eq!(paths[0], RoutePath::Mint("custom_method".to_string()));
329 }
330
331 #[test]
332 fn test_matching_route_paths_all() {
333 let paths = matching_route_paths("/v1/*").unwrap();
335
336 assert_eq!(paths.len(), RoutePath::all_known_paths().len());
338
339 assert!(paths.contains(&RoutePath::MintQuote(
341 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
342 )));
343 assert!(paths.contains(&RoutePath::Mint(
344 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
345 )));
346 assert!(paths.contains(&RoutePath::MeltQuote(
347 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
348 )));
349 assert!(paths.contains(&RoutePath::Melt(
350 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
351 )));
352 assert!(paths.contains(&RoutePath::MintQuote(
353 PaymentMethod::Known(KnownMethod::Bolt12).to_string()
354 )));
355 assert!(paths.contains(&RoutePath::Mint(
356 PaymentMethod::Known(KnownMethod::Bolt12).to_string()
357 )));
358 assert!(paths.contains(&RoutePath::Swap));
359 assert!(paths.contains(&RoutePath::Checkstate));
360 assert!(paths.contains(&RoutePath::Restore));
361 assert!(paths.contains(&RoutePath::MintBlindAuth));
362 }
363
364 #[test]
365 fn test_matching_route_paths_mint_only() {
366 let paths = matching_route_paths("/v1/mint/*").unwrap();
368
369 assert_eq!(paths.len(), 4);
371 assert!(paths.contains(&RoutePath::MintQuote(
372 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
373 )));
374 assert!(paths.contains(&RoutePath::Mint(
375 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
376 )));
377 assert!(paths.contains(&RoutePath::MintQuote(
378 PaymentMethod::Known(KnownMethod::Bolt12).to_string()
379 )));
380 assert!(paths.contains(&RoutePath::Mint(
381 PaymentMethod::Known(KnownMethod::Bolt12).to_string()
382 )));
383
384 assert!(!paths.contains(&RoutePath::MeltQuote(
386 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
387 )));
388 assert!(!paths.contains(&RoutePath::Melt(
389 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
390 )));
391 assert!(!paths.contains(&RoutePath::MeltQuote(
392 PaymentMethod::Known(KnownMethod::Bolt12).to_string()
393 )));
394 assert!(!paths.contains(&RoutePath::Melt(
395 PaymentMethod::Known(KnownMethod::Bolt12).to_string()
396 )));
397 assert!(!paths.contains(&RoutePath::Swap));
398 }
399
400 #[test]
401 fn test_matching_route_paths_quote_only() {
402 let paths = matching_route_paths("/v1/mint/quote/*").unwrap();
404
405 assert_eq!(paths.len(), 2);
407 assert!(paths.contains(&RoutePath::MintQuote(
408 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
409 )));
410 assert!(paths.contains(&RoutePath::MintQuote(
411 PaymentMethod::Known(KnownMethod::Bolt12).to_string()
412 )));
413
414 assert!(!paths.contains(&RoutePath::Mint(
416 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
417 )));
418 assert!(!paths.contains(&RoutePath::Melt(
419 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
420 )));
421 }
422
423 #[test]
424 fn test_matching_route_paths_no_match() {
425 let paths = matching_route_paths("/nonexistent/path").unwrap();
427
428 assert!(paths.is_empty());
430 }
431
432 #[test]
433 fn test_matching_route_paths_quote_bolt11_only() {
434 let paths = matching_route_paths("/v1/mint/quote/bolt11").unwrap();
436
437 assert_eq!(paths.len(), 1);
439 assert!(paths.contains(&RoutePath::MintQuote(
440 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
441 )));
442 }
443
444 #[test]
445 fn test_matching_route_paths_invalid_regex() {
446 let result = matching_route_paths("/*unclosed parenthesis");
448
449 assert!(result.is_err());
451 assert!(matches!(result.unwrap_err(), Error::InvalidPattern(_)));
452 }
453
454 #[test]
455 fn test_route_path_to_string() {
456 assert_eq!(
458 RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()).to_string(),
459 "/v1/mint/quote/bolt11"
460 );
461 assert_eq!(
462 RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string()).to_string(),
463 "/v1/mint/bolt11"
464 );
465 assert_eq!(
466 RoutePath::MeltQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()).to_string(),
467 "/v1/melt/quote/bolt11"
468 );
469 assert_eq!(
470 RoutePath::Melt(PaymentMethod::Known(KnownMethod::Bolt11).to_string()).to_string(),
471 "/v1/melt/bolt11"
472 );
473 assert_eq!(
474 RoutePath::MintQuote("paypal".to_string()).to_string(),
475 "/v1/mint/quote/paypal"
476 );
477 assert_eq!(RoutePath::Swap.to_string(), "/v1/swap");
478 assert_eq!(RoutePath::Checkstate.to_string(), "/v1/checkstate");
479 assert_eq!(RoutePath::Restore.to_string(), "/v1/restore");
480 assert_eq!(RoutePath::MintBlindAuth.to_string(), "/v1/auth/blind/mint");
481 }
482
483 #[test]
484 fn test_route_path_serialization() {
485 let json = serde_json::to_string(&RoutePath::Mint(
487 PaymentMethod::Known(KnownMethod::Bolt11).to_string(),
488 ))
489 .unwrap();
490 assert_eq!(json, "\"/v1/mint/bolt11\"");
491
492 let json = serde_json::to_string(&RoutePath::MintQuote("paypal".to_string())).unwrap();
493 assert_eq!(json, "\"/v1/mint/quote/paypal\"");
494
495 let path: RoutePath = serde_json::from_str("\"/v1/mint/bolt11\"").unwrap();
497 assert_eq!(
498 path,
499 RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string())
500 );
501
502 let path: RoutePath = serde_json::from_str("\"/v1/melt/quote/venmo\"").unwrap();
503 assert_eq!(path, RoutePath::MeltQuote("venmo".to_string()));
504
505 let original = RoutePath::Melt(PaymentMethod::Known(KnownMethod::Bolt12).to_string());
507 let json = serde_json::to_string(&original).unwrap();
508 let deserialized: RoutePath = serde_json::from_str(&json).unwrap();
509 assert_eq!(original, deserialized);
510 }
511
512 #[test]
513 fn test_settings_deserialize_direct_paths() {
514 let json = r#"{
515 "openid_discovery": "https://example.com/.well-known/openid-configuration",
516 "client_id": "client123",
517 "protected_endpoints": [
518 {
519 "method": "GET",
520 "path": "/v1/mint/bolt11"
521 },
522 {
523 "method": "POST",
524 "path": "/v1/swap"
525 }
526 ]
527 }"#;
528
529 let settings: Settings = serde_json::from_str(json).unwrap();
530
531 assert_eq!(
532 settings.openid_discovery,
533 "https://example.com/.well-known/openid-configuration"
534 );
535 assert_eq!(settings.client_id, "client123");
536 assert_eq!(settings.protected_endpoints.len(), 2);
537
538 let paths = settings
540 .protected_endpoints
541 .iter()
542 .map(|ep| (ep.method, ep.path.clone()))
543 .collect::<Vec<_>>();
544 assert!(paths.contains(&(
545 Method::Get,
546 RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string())
547 )));
548 assert!(paths.contains(&(Method::Post, RoutePath::Swap)));
549 }
550
551 #[test]
552 fn test_settings_deserialize_with_regex() {
553 let json = r#"{
554 "openid_discovery": "https://example.com/.well-known/openid-configuration",
555 "client_id": "client123",
556 "protected_endpoints": [
557 {
558 "method": "GET",
559 "path": "/v1/mint/*"
560 },
561 {
562 "method": "POST",
563 "path": "/v1/swap"
564 }
565 ]
566 }"#;
567
568 let settings: Settings = serde_json::from_str(json).unwrap();
569
570 assert_eq!(
571 settings.openid_discovery,
572 "https://example.com/.well-known/openid-configuration"
573 );
574 assert_eq!(settings.client_id, "client123");
575 assert_eq!(settings.protected_endpoints.len(), 5); let expected_protected: HashSet<ProtectedEndpoint> = HashSet::from_iter(vec![
578 ProtectedEndpoint::new(Method::Post, RoutePath::Swap),
579 ProtectedEndpoint::new(
580 Method::Get,
581 RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
582 ),
583 ProtectedEndpoint::new(
584 Method::Get,
585 RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string()),
586 ),
587 ProtectedEndpoint::new(
588 Method::Get,
589 RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt12).to_string()),
590 ),
591 ProtectedEndpoint::new(
592 Method::Get,
593 RoutePath::Mint(PaymentMethod::Known(KnownMethod::Bolt12).to_string()),
594 ),
595 ]);
596
597 let deserlized_protected = settings.protected_endpoints.into_iter().collect();
598
599 assert_eq!(expected_protected, deserlized_protected);
600 }
601
602 #[test]
603 fn test_settings_deserialize_invalid_regex() {
604 let json = r#"{
605 "openid_discovery": "https://example.com/.well-known/openid-configuration",
606 "client_id": "client123",
607 "protected_endpoints": [
608 {
609 "method": "GET",
610 "path": "/*wildcard_start"
611 }
612 ]
613 }"#;
614
615 let result = serde_json::from_str::<Settings>(json);
616 assert!(result.is_err());
617 }
618
619 #[test]
620 fn test_settings_deserialize_exact_path_match() {
621 let json = r#"{
622 "openid_discovery": "https://example.com/.well-known/openid-configuration",
623 "client_id": "client123",
624 "protected_endpoints": [
625 {
626 "method": "GET",
627 "path": "/v1/mint/quote/bolt11"
628 }
629 ]
630 }"#;
631
632 let settings: Settings = serde_json::from_str(json).unwrap();
633 assert_eq!(settings.protected_endpoints.len(), 1);
634 assert_eq!(settings.protected_endpoints[0].method, Method::Get);
635 assert_eq!(
636 settings.protected_endpoints[0].path,
637 RoutePath::MintQuote(PaymentMethod::Known(KnownMethod::Bolt11).to_string())
638 );
639 }
640
641 #[test]
642 fn test_settings_deserialize_all_paths() {
643 let json = r#"{
644 "openid_discovery": "https://example.com/.well-known/openid-configuration",
645 "client_id": "client123",
646 "protected_endpoints": [
647 {
648 "method": "GET",
649 "path": "/v1/*"
650 }
651 ]
652 }"#;
653
654 let settings: Settings = serde_json::from_str(json).unwrap();
655 assert_eq!(
656 settings.protected_endpoints.len(),
657 RoutePath::all_known_paths().len()
658 );
659 }
660
661 #[test]
662 fn test_matching_route_paths_empty_pattern() {
663 let paths = matching_route_paths("").unwrap();
665 assert!(paths.is_empty());
666 }
667
668 #[test]
669 fn test_matching_route_paths_just_slash() {
670 let paths = matching_route_paths("/").unwrap();
672 assert!(paths.is_empty());
673 }
674
675 #[test]
676 fn test_matching_route_paths_trailing_slash() {
677 let result = matching_route_paths("/v1/mint/*/");
681 assert!(result.is_err());
682 assert!(matches!(result.unwrap_err(), Error::InvalidPattern(_)));
683 }
684
685 #[test]
686 fn test_matching_route_paths_consecutive_wildcards() {
687 let result = matching_route_paths("**");
691 assert!(result.is_err());
692 assert!(matches!(result.unwrap_err(), Error::InvalidPattern(_)));
693 }
694
695 #[test]
696 fn test_matching_route_paths_method_specific() {
697 let paths = matching_route_paths("/v1/swap").unwrap();
701 assert_eq!(paths.len(), 1);
702 assert!(paths.contains(&RoutePath::Swap));
703 }
704
705 #[test]
706 fn test_settings_mixed_methods() {
707 let json = r#"{
709 "openid_discovery": "https://example.com/.well-known/openid-configuration",
710 "client_id": "client123",
711 "protected_endpoints": [
712 {
713 "method": "GET",
714 "path": "/v1/swap"
715 },
716 {
717 "method": "POST",
718 "path": "/v1/swap"
719 }
720 ]
721 }"#;
722
723 let settings: Settings = serde_json::from_str(json).unwrap();
724 assert_eq!(settings.protected_endpoints.len(), 2);
725
726 let methods: Vec<_> = settings
728 .protected_endpoints
729 .iter()
730 .map(|ep| ep.method)
731 .collect();
732 assert!(methods.contains(&Method::Get));
733 assert!(methods.contains(&Method::Post));
734
735 for ep in &settings.protected_endpoints {
737 assert_eq!(ep.path, RoutePath::Swap);
738 }
739 }
740
741 #[test]
742 fn test_matching_route_paths_melt_prefix() {
743 let paths = matching_route_paths("/v1/melt/*").unwrap();
745
746 assert_eq!(paths.len(), 4);
748 assert!(paths.contains(&RoutePath::Melt(
749 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
750 )));
751 assert!(paths.contains(&RoutePath::MeltQuote(
752 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
753 )));
754 assert!(paths.contains(&RoutePath::Melt(
755 PaymentMethod::Known(KnownMethod::Bolt12).to_string()
756 )));
757 assert!(paths.contains(&RoutePath::MeltQuote(
758 PaymentMethod::Known(KnownMethod::Bolt12).to_string()
759 )));
760
761 assert!(!paths.contains(&RoutePath::Mint(
763 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
764 )));
765 }
766
767 #[test]
768 fn test_matching_route_paths_static_exact() {
769 let swap_paths = matching_route_paths("/v1/swap").unwrap();
771 assert_eq!(swap_paths.len(), 1);
772 assert_eq!(swap_paths[0], RoutePath::Swap);
773
774 let checkstate_paths = matching_route_paths("/v1/checkstate").unwrap();
775 assert_eq!(checkstate_paths.len(), 1);
776 assert_eq!(checkstate_paths[0], RoutePath::Checkstate);
777
778 let restore_paths = matching_route_paths("/v1/restore").unwrap();
779 assert_eq!(restore_paths.len(), 1);
780 assert_eq!(restore_paths[0], RoutePath::Restore);
781
782 let ws_paths = matching_route_paths("/v1/ws").unwrap();
783 assert_eq!(ws_paths.len(), 1);
784 assert_eq!(ws_paths[0], RoutePath::Ws);
785 }
786
787 #[test]
788 fn test_matching_route_paths_auth_blind_mint() {
789 let paths = matching_route_paths("/v1/auth/blind/mint").unwrap();
791 assert_eq!(paths.len(), 1);
792 assert_eq!(paths[0], RoutePath::MintBlindAuth);
793 }
794
795 #[test]
796 fn test_settings_empty_endpoints() {
797 let json = r#"{
799 "openid_discovery": "https://example.com/.well-known/openid-configuration",
800 "client_id": "client123",
801 "protected_endpoints": []
802 }"#;
803
804 let settings: Settings = serde_json::from_str(json).unwrap();
805 assert!(settings.protected_endpoints.is_empty());
806 }
807
808 #[test]
809 fn test_settings_duplicate_paths() {
810 let json = r#"{
813 "openid_discovery": "https://example.com/.well-known/openid-configuration",
814 "client_id": "client123",
815 "protected_endpoints": [
816 {
817 "method": "POST",
818 "path": "/v1/swap"
819 },
820 {
821 "method": "POST",
822 "path": "/v1/swap"
823 }
824 ]
825 }"#;
826
827 let settings: Settings = serde_json::from_str(json).unwrap();
828 assert_eq!(settings.protected_endpoints.len(), 1);
829 assert_eq!(settings.protected_endpoints[0].method, Method::Post);
830 assert_eq!(settings.protected_endpoints[0].path, RoutePath::Swap);
831 }
832
833 #[test]
834 fn test_matching_route_paths_only_wildcard() {
835 let paths = matching_route_paths("*").unwrap();
837 assert_eq!(paths.len(), RoutePath::all_known_paths().len());
838 }
839
840 #[test]
841 fn test_matching_route_paths_wildcard_in_middle() {
842 let result = matching_route_paths("/v1/*/bolt11");
846 assert!(result.is_err());
847 assert!(matches!(result.unwrap_err(), Error::InvalidPattern(_)));
848 }
849
850 #[test]
851 fn test_exact_match_no_child_paths() {
852 let paths = matching_route_paths("/v1/mint").unwrap();
854
855 assert!(paths.is_empty());
858
859 assert!(!paths.contains(&RoutePath::Mint(
861 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
862 )));
863 assert!(!paths.contains(&RoutePath::MintQuote(
864 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
865 )));
866 }
867
868 #[test]
869 fn test_exact_match_no_extra_path() {
870 let paths = matching_route_paths("/v1/swap").unwrap();
874 assert_eq!(paths.len(), 1);
875 assert_eq!(paths[0], RoutePath::Swap);
876
877 assert!(!paths.contains(&RoutePath::Checkstate));
879 assert!(!paths.contains(&RoutePath::Restore));
880 }
881
882 #[test]
883 fn test_partial_prefix_matching() {
884 let paths = matching_route_paths("/v1/mi*").unwrap();
886
887 assert!(paths.contains(&RoutePath::Mint(
889 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
890 )));
891 assert!(paths.contains(&RoutePath::MintQuote(
892 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
893 )));
894
895 assert!(!paths.contains(&RoutePath::Melt(
897 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
898 )));
899 assert!(!paths.contains(&RoutePath::MeltQuote(
900 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
901 )));
902 }
903
904 #[test]
905 fn test_exact_match_wrong_payment_method() {
906 let paths = matching_route_paths("/v1/mint/quote/bolt11").unwrap();
908
909 assert_eq!(paths.len(), 1);
910 assert!(paths.contains(&RoutePath::MintQuote(
911 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
912 )));
913
914 assert!(!paths.contains(&RoutePath::MintQuote(
916 PaymentMethod::Known(KnownMethod::Bolt12).to_string()
917 )));
918
919 assert!(!paths.contains(&RoutePath::Mint(
921 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
922 )));
923 }
924
925 #[test]
926 fn test_prefix_match_wrong_category() {
927 let paths = matching_route_paths("/v1/mint/*").unwrap();
929
930 assert!(paths.contains(&RoutePath::Mint(
932 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
933 )));
934 assert!(paths.contains(&RoutePath::MintQuote(
935 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
936 )));
937
938 assert!(!paths.contains(&RoutePath::Melt(
940 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
941 )));
942 assert!(!paths.contains(&RoutePath::MeltQuote(
943 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
944 )));
945
946 assert!(!paths.contains(&RoutePath::Swap));
948 assert!(!paths.contains(&RoutePath::Checkstate));
949 }
950
951 #[test]
952 fn test_case_sensitivity() {
953 let paths_upper = matching_route_paths("/v1/MINT/*").unwrap();
955 let paths_lower = matching_route_paths("/v1/mint/*").unwrap();
956
957 assert!(paths_upper.is_empty());
959
960 assert_eq!(paths_lower.len(), 4);
962 }
963
964 #[test]
965 fn test_negative_assertions_comprehensive() {
966 let bolt11_paths = matching_route_paths("/v1/mint/quote/bolt11").unwrap();
970 assert!(!bolt11_paths.contains(&RoutePath::MintQuote(
971 PaymentMethod::Known(KnownMethod::Bolt12).to_string()
972 )));
973
974 let mint_paths = matching_route_paths("/v1/mint/*").unwrap();
976 assert!(!mint_paths.contains(&RoutePath::Melt(
977 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
978 )));
979 assert!(!mint_paths.contains(&RoutePath::MeltQuote(
980 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
981 )));
982
983 let swap_paths = matching_route_paths("/v1/swap").unwrap();
985 assert!(!swap_paths.contains(&RoutePath::Checkstate));
986 assert!(!swap_paths.contains(&RoutePath::Restore));
987 assert!(!swap_paths.contains(&RoutePath::MintBlindAuth));
988
989 assert!(matching_route_paths("/V1/SWAP").unwrap().is_empty());
991 assert!(matching_route_paths("/V1/MINT/*").unwrap().is_empty());
992
993 assert!(matching_route_paths("/unknown/path").unwrap().is_empty());
995 assert!(matching_route_paths("/invalid").unwrap().is_empty());
996 }
997
998 #[test]
999 fn test_prefix_vs_exact_boundary() {
1000 let paths = matching_route_paths("/v1/mint/quote/*").unwrap();
1002
1003 assert!(!paths.is_empty()); assert!(paths.contains(&RoutePath::MintQuote(
1010 PaymentMethod::Known(KnownMethod::Bolt11).to_string()
1011 )));
1012 assert!(paths.contains(&RoutePath::MintQuote(
1013 PaymentMethod::Known(KnownMethod::Bolt12).to_string()
1014 )));
1015
1016 assert_eq!(paths.len(), 2);
1019 }
1020}