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