diff --git a/system/web/services/ModuleService.cfc b/system/web/services/ModuleService.cfc index 88aa0b51a..30c3a3a21 100755 --- a/system/web/services/ModuleService.cfc +++ b/system/web/services/ModuleService.cfc @@ -843,7 +843,7 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { var conventionsRouteExists = mConfig.router .getRoutes() .findAll( ( item ) => { - return ( item.pattern == "/:handler/:action" || item.pattern == ":handler/:action" ) + return reFindNoCase( "^/?\:handler/\:action\??/?$", item.pattern ) } ) if ( arrayLen( conventionsRouteExists ) == 0 ) { mConfig.router.route( "/:handler/:action?" ).end() diff --git a/system/web/services/RoutingService.cfc b/system/web/services/RoutingService.cfc index 8b37821f9..7450a97f2 100644 --- a/system/web/services/RoutingService.cfc +++ b/system/web/services/RoutingService.cfc @@ -634,6 +634,7 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { // build routing argument struct based on module/namespace context var contextRouting = { action : reReplaceNoCase( requestString, results.route.regexpattern, "" ), + domain : arguments.domain, event : arguments.event, excludedPatterns : arguments.excludedPatterns }; @@ -659,10 +660,14 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { // process context discovery of incoming pattern var contextRoute = findRoute( argumentCollection = contextRouting ); - // Return if route Not found. if ( !contextRoute.route.isEmpty() ) { return contextRoute; } + + // A module or namespace mount point is not itself an executable route. + // Return the empty nested result so a failed domain or condition match + // cannot fall back to the mount point. + return contextRoute; } // Save current routed details in PRC @@ -695,6 +700,7 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { // Return found Route recursively. return findRoute( action = packagedRequestString, + domain = arguments.domain, event = arguments.event, module = arguments.module, excludedPatterns = arguments.excludedPatterns diff --git a/tests/specs/integration/ModuleSpec.cfc b/tests/specs/integration/ModuleSpec.cfc index 117308e44..704c7f40c 100644 --- a/tests/specs/integration/ModuleSpec.cfc +++ b/tests/specs/integration/ModuleSpec.cfc @@ -111,6 +111,16 @@ component extends="tests.resources.BaseIntegrationTest" { expect( routing ).notToBeEmpty(); } ); + + then( "an explicit convention route should not be registered twice", () => { + var routingService = getController().getRoutingService(); + var routing = routingService.getModuleRoutes( "resourcesTest" ); + var conventionRoutes = routing.filter( ( item ) => { + return reFindNoCase( "^/?\:handler/\:action\??/?$", item.pattern ); + } ); + + expect( conventionRoutes ).toHaveLength( 1 ); + } ); } ); } ); diff --git a/tests/specs/web/routing/RoutingServiceTest.cfc b/tests/specs/web/routing/RoutingServiceTest.cfc index eaf516794..d6a281ad5 100755 --- a/tests/specs/web/routing/RoutingServiceTest.cfc +++ b/tests/specs/web/routing/RoutingServiceTest.cfc @@ -281,6 +281,53 @@ expect( discoveredEventPOST ).toBe( "api-v1:MyOtherHandler.create" ); } ); + + it( "preserves the request domain when resolving module routes", function(){ + var moduleName = "domainRoutingTest"; + var router = getController().getRoutingService().getRouter(); + var modules = getController().getSetting( "modules" ); + var mockEvent = createMock( "coldbox.system.web.context.RequestContext" ).init( + controller = getController(), + properties = { + defaultLayout : "Main.cfm", + defaultView : "", + eventName : "event", + modules : {} + } + ); + + modules[ moduleName ] = { resources : [], routes : [] }; + + try { + router.addModuleRoutes( + pattern = "/domain-module", + module = moduleName, + append = false + ); + router.addRoute( + pattern = "/ceremony", + event = "Passkeys.authenticate", + domain = "allowed.example", + module = moduleName + ); + var allowed = routingService.findRoute( + action = "/domain-module/ceremony", + domain = "allowed.example", + event = mockEvent + ); + var denied = routingService.findRoute( + action = "/domain-module/ceremony", + domain = "denied.example", + event = mockEvent + ); + + expect( allowed.route.event ).toBe( "Passkeys.authenticate" ); + expect( denied.route ).toBeEmpty(); + } finally { + router.removeModuleRoutes( moduleName ); + structDelete( modules, moduleName ); + } + } ); } ); describe( "route-scoped middleware (runRouteMiddleware())", function(){