@@ -292,11 +292,37 @@ impl IntegrationRegistry {
292292 }
293293 }
294294
295+ /// Find a matching route, supporting wildcard patterns (routes ending with /*).
296+ fn find_route ( & self , method : & Method , path : & str ) -> Option < & RouteValue > {
297+ // First try exact match
298+ let key = ( method. clone ( ) , path. to_string ( ) ) ;
299+ if let Some ( route_value) = self . inner . route_map . get ( & key) {
300+ return Some ( route_value) ;
301+ }
302+
303+ // If no exact match, try wildcard matching
304+ // Routes ending with /* should match any path with that prefix + additional segments
305+ for ( ( route_method, route_path) , route_value) in & self . inner . route_map {
306+ if route_method != method {
307+ continue ;
308+ }
309+
310+ // Check if this is a wildcard route (ends with /*)
311+ if let Some ( prefix) = route_path. strip_suffix ( "/*" ) {
312+ // Match if the incoming path starts with the prefix followed by /
313+ // This ensures /api/* matches /api/v1 but not /api or /apiv1
314+ if path. starts_with ( prefix) && path. len ( ) > prefix. len ( ) && path[ prefix. len ( ) ..] . starts_with ( '/' ) {
315+ return Some ( route_value) ;
316+ }
317+ }
318+ }
319+
320+ None
321+ }
322+
295323 /// Return true when any proxy is registered for the provided route.
296324 pub fn has_route ( & self , method : & Method , path : & str ) -> bool {
297- self . inner
298- . route_map
299- . contains_key ( & ( method. clone ( ) , path. to_string ( ) ) )
325+ self . find_route ( method, path) . is_some ( )
300326 }
301327
302328 /// Dispatch a proxy request when an integration handles the path.
@@ -307,11 +333,7 @@ impl IntegrationRegistry {
307333 settings : & Settings ,
308334 req : Request ,
309335 ) -> Option < Result < Response , Report < TrustedServerError > > > {
310- if let Some ( ( proxy, _) ) = self
311- . inner
312- . route_map
313- . get ( & ( method. clone ( ) , path. to_string ( ) ) )
314- {
336+ if let Some ( ( proxy, _) ) = self . find_route ( method, path) {
315337 Some ( proxy. handle ( settings, req) . await )
316338 } else {
317339 None
@@ -399,4 +421,189 @@ impl IntegrationRegistry {
399421 } ) ,
400422 }
401423 }
424+
425+ #[ cfg( test) ]
426+ pub fn from_routes ( routes : HashMap < RouteKey , RouteValue > ) -> Self {
427+ Self {
428+ inner : Arc :: new ( IntegrationRegistryInner {
429+ route_map : routes,
430+ routes : Vec :: new ( ) ,
431+ html_rewriters : Vec :: new ( ) ,
432+ script_rewriters : Vec :: new ( ) ,
433+ } ) ,
434+ }
435+ }
436+ }
437+
438+ #[ cfg( test) ]
439+ mod tests {
440+ use super :: * ;
441+
442+ // Mock integration proxy for testing
443+ struct MockProxy ;
444+
445+ #[ async_trait( ?Send ) ]
446+ impl IntegrationProxy for MockProxy {
447+ fn routes ( & self ) -> Vec < IntegrationEndpoint > {
448+ vec ! [ ]
449+ }
450+
451+ async fn handle (
452+ & self ,
453+ _settings : & Settings ,
454+ _req : Request ,
455+ ) -> Result < Response , Report < TrustedServerError > > {
456+ Ok ( Response :: new ( ) )
457+ }
458+ }
459+
460+ #[ test]
461+ fn test_exact_route_matching ( ) {
462+ let mut routes = HashMap :: new ( ) ;
463+ routes. insert (
464+ ( Method :: GET , "/integrations/test/exact" . to_string ( ) ) ,
465+ ( Arc :: new ( MockProxy ) as Arc < dyn IntegrationProxy > , "test" ) ,
466+ ) ;
467+
468+ let registry = IntegrationRegistry :: from_routes ( routes) ;
469+
470+ // Should match exact route
471+ assert ! ( registry. has_route( & Method :: GET , "/integrations/test/exact" ) ) ;
472+
473+ // Should not match different paths
474+ assert ! ( !registry. has_route( & Method :: GET , "/integrations/test/other" ) ) ;
475+ assert ! ( !registry. has_route( & Method :: GET , "/integrations/test/exact/nested" ) ) ;
476+
477+ // Should not match different methods
478+ assert ! ( !registry. has_route( & Method :: POST , "/integrations/test/exact" ) ) ;
479+ }
480+
481+ #[ test]
482+ fn test_wildcard_route_matching ( ) {
483+ let mut routes = HashMap :: new ( ) ;
484+ routes. insert (
485+ ( Method :: GET , "/integrations/lockr/api/*" . to_string ( ) ) ,
486+ ( Arc :: new ( MockProxy ) as Arc < dyn IntegrationProxy > , "lockr" ) ,
487+ ) ;
488+
489+ let registry = IntegrationRegistry :: from_routes ( routes) ;
490+
491+ // Should match paths under the wildcard prefix
492+ assert ! ( registry. has_route( & Method :: GET , "/integrations/lockr/api/settings" ) ) ;
493+ assert ! ( registry. has_route(
494+ & Method :: GET ,
495+ "/integrations/lockr/api/publisher/app/v1/identityLockr/settings"
496+ ) ) ;
497+ assert ! ( registry. has_route( & Method :: GET , "/integrations/lockr/api/page-view" ) ) ;
498+ assert ! ( registry. has_route( & Method :: GET , "/integrations/lockr/api/a/b/c/d/e" ) ) ;
499+
500+ // Should not match paths that don't start with the prefix
501+ assert ! ( !registry. has_route( & Method :: GET , "/integrations/lockr/sdk" ) ) ;
502+ assert ! ( !registry. has_route( & Method :: GET , "/integrations/lockr/other" ) ) ;
503+ assert ! ( !registry. has_route( & Method :: GET , "/integrations/other/api/settings" ) ) ;
504+
505+ // Should not match different methods
506+ assert ! ( !registry. has_route( & Method :: POST , "/integrations/lockr/api/settings" ) ) ;
507+ }
508+
509+ #[ test]
510+ fn test_wildcard_and_exact_routes_coexist ( ) {
511+ let mut routes = HashMap :: new ( ) ;
512+ routes. insert (
513+ ( Method :: GET , "/integrations/test/api/*" . to_string ( ) ) ,
514+ ( Arc :: new ( MockProxy ) as Arc < dyn IntegrationProxy > , "test" ) ,
515+ ) ;
516+ routes. insert (
517+ ( Method :: GET , "/integrations/test/exact" . to_string ( ) ) ,
518+ ( Arc :: new ( MockProxy ) as Arc < dyn IntegrationProxy > , "test" ) ,
519+ ) ;
520+
521+ let registry = IntegrationRegistry :: from_routes ( routes) ;
522+
523+ // Exact route should match
524+ assert ! ( registry. has_route( & Method :: GET , "/integrations/test/exact" ) ) ;
525+
526+ // Wildcard routes should match
527+ assert ! ( registry. has_route( & Method :: GET , "/integrations/test/api/anything" ) ) ;
528+ assert ! ( registry. has_route( & Method :: GET , "/integrations/test/api/nested/path" ) ) ;
529+
530+ // Non-matching should fail
531+ assert ! ( !registry. has_route( & Method :: GET , "/integrations/test/other" ) ) ;
532+ }
533+
534+ #[ test]
535+ fn test_multiple_wildcard_routes ( ) {
536+ let mut routes = HashMap :: new ( ) ;
537+ routes. insert (
538+ ( Method :: GET , "/integrations/lockr/api/*" . to_string ( ) ) ,
539+ ( Arc :: new ( MockProxy ) as Arc < dyn IntegrationProxy > , "lockr" ) ,
540+ ) ;
541+ routes. insert (
542+ ( Method :: POST , "/integrations/lockr/api/*" . to_string ( ) ) ,
543+ ( Arc :: new ( MockProxy ) as Arc < dyn IntegrationProxy > , "lockr" ) ,
544+ ) ;
545+ routes. insert (
546+ ( Method :: GET , "/integrations/testlight/api/*" . to_string ( ) ) ,
547+ ( Arc :: new ( MockProxy ) as Arc < dyn IntegrationProxy > , "testlight" ) ,
548+ ) ;
549+
550+ let registry = IntegrationRegistry :: from_routes ( routes) ;
551+
552+ // Lockr GET routes should match
553+ assert ! ( registry. has_route( & Method :: GET , "/integrations/lockr/api/settings" ) ) ;
554+
555+ // Lockr POST routes should match
556+ assert ! ( registry. has_route( & Method :: POST , "/integrations/lockr/api/settings" ) ) ;
557+
558+ // Testlight routes should match
559+ assert ! ( registry. has_route( & Method :: GET , "/integrations/testlight/api/auction" ) ) ;
560+ assert ! ( registry. has_route( & Method :: GET , "/integrations/testlight/api/any-path" ) ) ;
561+
562+ // Cross-integration paths should not match
563+ assert ! ( !registry. has_route( & Method :: GET , "/integrations/lockr/other-endpoint" ) ) ;
564+ assert ! ( !registry. has_route( & Method :: GET , "/integrations/other/api/test" ) ) ;
565+ }
566+
567+ #[ test]
568+ fn test_wildcard_preserves_casing ( ) {
569+ let mut routes = HashMap :: new ( ) ;
570+ routes. insert (
571+ ( Method :: GET , "/integrations/lockr/api/*" . to_string ( ) ) ,
572+ ( Arc :: new ( MockProxy ) as Arc < dyn IntegrationProxy > , "lockr" ) ,
573+ ) ;
574+
575+ let registry = IntegrationRegistry :: from_routes ( routes) ;
576+
577+ // Should match with camelCase preserved
578+ assert ! ( registry. has_route(
579+ & Method :: GET ,
580+ "/integrations/lockr/api/publisher/app/v1/identityLockr/settings"
581+ ) ) ;
582+ assert ! ( registry. has_route(
583+ & Method :: GET ,
584+ "/integrations/lockr/api/publisher/app/v1/identitylockr/settings"
585+ ) ) ;
586+ }
587+
588+ #[ test]
589+ fn test_wildcard_edge_cases ( ) {
590+ let mut routes = HashMap :: new ( ) ;
591+ routes. insert (
592+ ( Method :: GET , "/api/*" . to_string ( ) ) ,
593+ ( Arc :: new ( MockProxy ) as Arc < dyn IntegrationProxy > , "test" ) ,
594+ ) ;
595+
596+ let registry = IntegrationRegistry :: from_routes ( routes) ;
597+
598+ // Should match paths under /api/
599+ assert ! ( registry. has_route( & Method :: GET , "/api/v1" ) ) ;
600+ assert ! ( registry. has_route( & Method :: GET , "/api/v1/users" ) ) ;
601+
602+ // Should not match /api without trailing content
603+ // The current implementation requires a / after the prefix
604+ assert ! ( !registry. has_route( & Method :: GET , "/api" ) ) ;
605+
606+ // Should not match partial prefix matches
607+ assert ! ( !registry. has_route( & Method :: GET , "/apiv1" ) ) ;
608+ }
402609}
0 commit comments