diff --git a/CHANGELOG.md b/CHANGELOG.md index 97cff1063d632..ec1eb17540246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - SegRep with Remote: Add hook for publishing checkpoint notifications after segment upload to remote store ([#7394](https://github.com/opensearch-project/OpenSearch/pull/7394)) - Provide mechanism to configure XContent parsing constraints (after update to Jackson 2.15.0 and above) ([#7550](https://github.com/opensearch-project/OpenSearch/pull/7550)) - Support to clear filecache using clear indices cache API ([#7498](https://github.com/opensearch-project/OpenSearch/pull/7498)) +- Create NamedRoute to map extension routes to a shortened name ([#6870](https://github.com/opensearch-project/OpenSearch/pull/6870)) ### Dependencies - Bump `com.netflix.nebula:gradle-info-plugin` from 12.0.0 to 12.1.3 (#7564) diff --git a/server/src/main/java/org/opensearch/action/ActionModule.java b/server/src/main/java/org/opensearch/action/ActionModule.java index f7bc770fbad33..168fbae84fdf4 100644 --- a/server/src/main/java/org/opensearch/action/ActionModule.java +++ b/server/src/main/java/org/opensearch/action/ActionModule.java @@ -306,6 +306,7 @@ import org.opensearch.persistent.UpdatePersistentTaskStatusAction; import org.opensearch.plugins.ActionPlugin; import org.opensearch.plugins.ActionPlugin.ActionHandler; +import org.opensearch.rest.NamedRoute; import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; import org.opensearch.rest.RestHeaderDefinition; @@ -452,6 +453,7 @@ import org.opensearch.rest.action.search.RestPutSearchPipelineAction; import org.opensearch.rest.action.search.RestSearchAction; import org.opensearch.rest.action.search.RestSearchScrollAction; +import org.opensearch.rest.extensions.RestSendToExtensionAction; import org.opensearch.tasks.Task; import org.opensearch.threadpool.ThreadPool; import org.opensearch.usage.UsageService; @@ -460,8 +462,10 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentSkipListSet; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.function.UnaryOperator; @@ -1028,6 +1032,12 @@ public static class DynamicActionRegistry { // at times other than node bootstrap. private final Map, TransportAction> registry = new ConcurrentHashMap<>(); + // A dynamic registry to add or remove Route / RestSendToExtensionAction pairs + // at times other than node bootstrap. + private final Map routeRegistry = new ConcurrentHashMap<>(); + + private final Set registeredActionNames = new ConcurrentSkipListSet<>(); + /** * Register the immutable actions in the registry. * @@ -1035,6 +1045,9 @@ public static class DynamicActionRegistry { */ public void registerUnmodifiableActionMap(Map actions) { this.actions = actions; + for (ActionType action : actions.keySet()) { + registeredActionNames.add(action.name()); + } } /** @@ -1049,6 +1062,7 @@ public void registerDynamicAction(ActionType action, TransportAction tr if (actions.containsKey(action) || registry.putIfAbsent(action, transportAction) != null) { throw new IllegalArgumentException("action [" + action.name() + "] already registered"); } + registeredActionNames.add(action.name()); } /** @@ -1061,6 +1075,16 @@ public void unregisterDynamicAction(ActionType action) { if (registry.remove(action) == null) { throw new IllegalArgumentException("action [" + action.name() + "] was not registered"); } + registeredActionNames.remove(action.name()); + } + + /** + * Checks to see if an action is registered provided an action name + * + * @param actionName The name of the action to check + */ + public boolean isActionRegistered(String actionName) { + return registeredActionNames.contains(actionName); } /** @@ -1076,5 +1100,54 @@ public void unregisterDynamicAction(ActionType action) { } return registry.get(action); } + + /** + * Add a dynamic action to the registry. + * + * @param route The route instance to add + * @param action The corresponding instance of RestSendToExtensionAction to execute + */ + public void registerDynamicRoute(RestHandler.Route route, RestSendToExtensionAction action) { + requireNonNull(route, "route is required"); + requireNonNull(action, "action is required"); + Optional routeName = Optional.empty(); + if (route instanceof NamedRoute) { + routeName = Optional.of(((NamedRoute) route).name()); + if (isActionRegistered(routeName.get()) || registeredActionNames.contains(routeName.get())) { + throw new IllegalArgumentException("route [" + route + "] already registered"); + } + } + if (routeRegistry.containsKey(route)) { + throw new IllegalArgumentException("route [" + route + "] already registered"); + } + routeRegistry.put(route, action); + routeName.ifPresent(registeredActionNames::add); + } + + /** + * Remove a dynamic route from the registry. + * + * @param route The route to remove + */ + public void unregisterDynamicRoute(RestHandler.Route route) { + requireNonNull(route, "route is required"); + if (routeRegistry.remove(route) == null) { + throw new IllegalArgumentException("action [" + route + "] was not registered"); + } + if (route instanceof NamedRoute) { + registeredActionNames.remove(((NamedRoute) route).name()); + } + } + + /** + * Gets the {@link RestSendToExtensionAction} instance corresponding to the {@link RestHandler.Route} instance. + * + * @param route The {@link RestHandler.Route}. + * @return the corresponding {@link RestSendToExtensionAction} if it is registered, null otherwise. + */ + @SuppressWarnings("unchecked") + public RestSendToExtensionAction get(RestHandler.Route route) { + return routeRegistry.get(route); + } } } diff --git a/server/src/main/java/org/opensearch/extensions/ExtensionsManager.java b/server/src/main/java/org/opensearch/extensions/ExtensionsManager.java index f83e371f58943..337dbdd06e1af 100644 --- a/server/src/main/java/org/opensearch/extensions/ExtensionsManager.java +++ b/server/src/main/java/org/opensearch/extensions/ExtensionsManager.java @@ -33,6 +33,7 @@ import org.opensearch.OpenSearchException; import org.opensearch.Version; import org.opensearch.action.ActionModule; +import org.opensearch.action.ActionModule.DynamicActionRegistry; import org.opensearch.action.admin.cluster.state.ClusterStateResponse; import org.opensearch.client.node.NodeClient; import org.opensearch.cluster.ClusterSettingsResponse; @@ -108,7 +109,6 @@ public static enum OpenSearchRequestType { private final Path extensionsPath; private ExtensionTransportActionsHandler extensionTransportActionsHandler; - private Map extensionSettingsMap; private Map initializedExtensions; private Map extensionIdMap; @@ -182,7 +182,7 @@ public void initializeServicesAndRestHandler( actionModule, this ); - registerRequestHandler(); + registerRequestHandler(actionModule.getDynamicActionRegistry()); } /** @@ -223,14 +223,16 @@ public ExtensionActionResponse handleTransportRequest(ExtensionActionRequest req return extensionTransportActionsHandler.sendTransportRequestToExtension(request); } - private void registerRequestHandler() { + private void registerRequestHandler(DynamicActionRegistry dynamicActionRegistry) { transportService.registerRequestHandler( REQUEST_EXTENSION_REGISTER_REST_ACTIONS, ThreadPool.Names.GENERIC, false, false, RegisterRestActionsRequest::new, - ((request, channel, task) -> channel.sendResponse(restActionsRequestHandler.handleRegisterRestActionsRequest(request))) + ((request, channel, task) -> channel.sendResponse( + restActionsRequestHandler.handleRegisterRestActionsRequest(request, dynamicActionRegistry) + )) ); transportService.registerRequestHandler( REQUEST_EXTENSION_REGISTER_CUSTOM_SETTINGS, diff --git a/server/src/main/java/org/opensearch/extensions/rest/RestActionsRequestHandler.java b/server/src/main/java/org/opensearch/extensions/rest/RestActionsRequestHandler.java index 790beaef0a969..37638f2a333d5 100644 --- a/server/src/main/java/org/opensearch/extensions/rest/RestActionsRequestHandler.java +++ b/server/src/main/java/org/opensearch/extensions/rest/RestActionsRequestHandler.java @@ -8,10 +8,12 @@ package org.opensearch.extensions.rest; +import org.opensearch.action.ActionModule.DynamicActionRegistry; import org.opensearch.extensions.AcknowledgedResponse; import org.opensearch.extensions.DiscoveryExtensionNode; import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; +import org.opensearch.rest.extensions.RestSendToExtensionAction; import org.opensearch.transport.TransportResponse; import org.opensearch.transport.TransportService; @@ -52,9 +54,17 @@ public RestActionsRequestHandler( * @return A {@link AcknowledgedResponse} indicating success. * @throws Exception if the request is not handled properly. */ - public TransportResponse handleRegisterRestActionsRequest(RegisterRestActionsRequest restActionsRequest) throws Exception { + public TransportResponse handleRegisterRestActionsRequest( + RegisterRestActionsRequest restActionsRequest, + DynamicActionRegistry dynamicActionRegistry + ) throws Exception { DiscoveryExtensionNode discoveryExtensionNode = extensionIdMap.get(restActionsRequest.getUniqueId()); - RestHandler handler = new RestSendToExtensionAction(restActionsRequest, discoveryExtensionNode, transportService); + RestHandler handler = new RestSendToExtensionAction( + restActionsRequest, + discoveryExtensionNode, + transportService, + dynamicActionRegistry + ); restController.registerHandler(handler); return new AcknowledgedResponse(true); } diff --git a/server/src/main/java/org/opensearch/extensions/rest/RouteHandler.java b/server/src/main/java/org/opensearch/extensions/rest/RouteHandler.java new file mode 100644 index 0000000000000..189d67c120189 --- /dev/null +++ b/server/src/main/java/org/opensearch/extensions/rest/RouteHandler.java @@ -0,0 +1,69 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.extensions.rest; + +import java.util.function.Function; + +import org.opensearch.rest.RestHandler.Route; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestRequest.Method; + +/** + * A subclass of {@link Route} that includes a handler method for that route. + */ +public class RouteHandler extends Route { + + private final String name; + + private final Function responseHandler; + + /** + * Handle the method and path with the specified handler. + * + * @param method The {@link Method} to handle. + * @param path The path to handle. + * @param handler The method which handles the method and path. + */ + public RouteHandler(Method method, String path, Function handler) { + super(method, path); + this.responseHandler = handler; + this.name = null; + } + + /** + * Handle the method and path with the specified handler. + * + * @param name The name of the handler. + * @param method The {@link Method} to handle. + * @param path The path to handle. + * @param handler The method which handles the method and path. + */ + public RouteHandler(String name, Method method, String path, Function handler) { + super(method, path); + this.responseHandler = handler; + this.name = name; + } + + /** + * Executes the handler for this route. + * + * @param request The request to handle + * @return the {@link ExtensionRestResponse} result from the handler for this route. + */ + public ExtensionRestResponse handleRequest(RestRequest request) { + return responseHandler.apply(request); + } + + /** + * The name of the RouteHandler. Must be unique across route handlers. + */ + public String name() { + return this.name; + } +} diff --git a/server/src/main/java/org/opensearch/rest/NamedRoute.java b/server/src/main/java/org/opensearch/rest/NamedRoute.java new file mode 100644 index 0000000000000..f5eaafcd04056 --- /dev/null +++ b/server/src/main/java/org/opensearch/rest/NamedRoute.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.rest; + +import org.opensearch.OpenSearchException; + +/** + * A named Route + * + * @opensearch.internal + */ +public class NamedRoute extends RestHandler.Route { + private static final String VALID_ACTION_NAME_PATTERN = "^[a-zA-Z0-9:/*_]*$"; + static final int MAX_LENGTH_OF_ACTION_NAME = 250; + + private final String name; + + public boolean isValidRouteName(String routeName) { + if (routeName == null || routeName.isBlank() || routeName.length() > MAX_LENGTH_OF_ACTION_NAME) { + return false; + } + return routeName.matches(VALID_ACTION_NAME_PATTERN); + } + + public NamedRoute(RestRequest.Method method, String path, String name) { + super(method, path); + if (!isValidRouteName(name)) { + throw new OpenSearchException( + "Invalid route name specified. The route name may include the following characters" + + " 'a-z', 'A-Z', '0-9', ':', '/', '*', '_' and be less than " + + MAX_LENGTH_OF_ACTION_NAME + + " characters" + ); + } + this.name = name; + } + + /** + * The name of the Route. Must be unique across Route. + */ + public String name() { + return this.name; + } + + @Override + public String toString() { + return "NamedRoute [method=" + method + ", path=" + path + ", name=" + name + "]"; + } +} diff --git a/server/src/main/java/org/opensearch/rest/RestHandler.java b/server/src/main/java/org/opensearch/rest/RestHandler.java index 993a548fb0039..7832649e8ad32 100644 --- a/server/src/main/java/org/opensearch/rest/RestHandler.java +++ b/server/src/main/java/org/opensearch/rest/RestHandler.java @@ -184,8 +184,8 @@ public boolean allowSystemIndexAccessByDefault() { */ class Route { - private final String path; - private final Method method; + protected final String path; + protected final Method method; public Route(Method method, String path) { this.path = path; @@ -196,9 +196,37 @@ public String getPath() { return path; } + public String getPathWithPathParamsReplaced() { + return path.replaceAll("(?<=\\{).*?(?=\\})", "path_param"); + } + public Method getMethod() { return method; } + + @Override + public int hashCode() { + String routeStr = "Route [method=" + method + ", path=" + getPathWithPathParamsReplaced() + "]"; + return routeStr.hashCode(); + } + + @Override + public String toString() { + return "Route [method=" + method + ", path=" + path + "]"; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Route that = (Route) o; + return Objects.equals(method, that.method) + && Objects.equals(getPathWithPathParamsReplaced(), that.getPathWithPathParamsReplaced()); + } } /** diff --git a/server/src/main/java/org/opensearch/extensions/rest/RestSendToExtensionAction.java b/server/src/main/java/org/opensearch/rest/extensions/RestSendToExtensionAction.java similarity index 89% rename from server/src/main/java/org/opensearch/extensions/rest/RestSendToExtensionAction.java rename to server/src/main/java/org/opensearch/rest/extensions/RestSendToExtensionAction.java index 9c2f77d4053d3..8c4e3f4b42412 100644 --- a/server/src/main/java/org/opensearch/extensions/rest/RestSendToExtensionAction.java +++ b/server/src/main/java/org/opensearch/rest/extensions/RestSendToExtensionAction.java @@ -6,18 +6,23 @@ * compatible open source license. */ -package org.opensearch.extensions.rest; +package org.opensearch.rest.extensions; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.opensearch.action.ActionModule.DynamicActionRegistry; import org.opensearch.client.node.NodeClient; import org.opensearch.common.bytes.BytesReference; import org.opensearch.common.io.stream.StreamInput; import org.opensearch.common.xcontent.XContentType; import org.opensearch.extensions.DiscoveryExtensionNode; import org.opensearch.extensions.ExtensionsManager; +import org.opensearch.extensions.rest.ExtensionRestRequest; +import org.opensearch.extensions.rest.RegisterRestActionsRequest; +import org.opensearch.extensions.rest.RestExecuteOnExtensionResponse; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.NamedRoute; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; import org.opensearch.rest.RestStatus; @@ -33,6 +38,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.concurrent.CompletableFuture; @@ -78,7 +84,8 @@ public String getName() { public RestSendToExtensionAction( RegisterRestActionsRequest restActionsRequest, DiscoveryExtensionNode discoveryExtensionNode, - TransportService transportService + TransportService transportService, + DynamicActionRegistry dynamicActionRegistry ) { this.pathPrefix = "/_extensions/_" + restActionsRequest.getUniqueId(); RestRequest.Method method; @@ -86,15 +93,30 @@ public RestSendToExtensionAction( List restActionsAsRoutes = new ArrayList<>(); for (String restAction : restActionsRequest.getRestActions()) { - int delim = restAction.indexOf(' '); + Optional name = Optional.empty(); + String[] parts = restAction.split(" "); + if (parts.length < 2) { + throw new IllegalArgumentException("REST action must contain at least a REST method and route"); + } try { - method = RestRequest.Method.valueOf(restAction.substring(0, delim)); - path = pathPrefix + restAction.substring(delim).trim(); + method = RestRequest.Method.valueOf(parts[0].trim()); + path = pathPrefix + parts[1].trim(); + if (parts.length > 2) { + name = Optional.of(parts[2].trim()); + } } catch (IndexOutOfBoundsException | IllegalArgumentException e) { throw new IllegalArgumentException(restAction + " does not begin with a valid REST method"); } logger.info("Registering: " + method + " " + path); - restActionsAsRoutes.add(new Route(method, path)); + if (name.isPresent()) { + NamedRoute nr = new NamedRoute(method, path, name.get()); + restActionsAsRoutes.add(nr); + dynamicActionRegistry.registerDynamicRoute(nr, this); + } else { + Route r = new Route(method, path); + restActionsAsRoutes.add(r); + dynamicActionRegistry.registerDynamicRoute(r, this); + } } this.routes = unmodifiableList(restActionsAsRoutes); diff --git a/server/src/main/java/org/opensearch/rest/extensions/package-info.java b/server/src/main/java/org/opensearch/rest/extensions/package-info.java new file mode 100644 index 0000000000000..64b92e8b5c149 --- /dev/null +++ b/server/src/main/java/org/opensearch/rest/extensions/package-info.java @@ -0,0 +1,10 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/** REST classes for the extensions package. OpenSearch extensions provide extensibility to OpenSearch.*/ +package org.opensearch.rest.extensions; diff --git a/server/src/test/java/org/opensearch/action/ActionModuleTests.java b/server/src/test/java/org/opensearch/action/ActionModuleTests.java index 41375dc005ef3..66af9ebfd814f 100644 --- a/server/src/test/java/org/opensearch/action/ActionModuleTests.java +++ b/server/src/test/java/org/opensearch/action/ActionModuleTests.java @@ -33,7 +33,6 @@ package org.opensearch.action; import java.util.ArrayList; -import org.opensearch.action.ActionModule.DynamicActionRegistry; import org.opensearch.action.main.MainAction; import org.opensearch.action.main.TransportMainAction; import org.opensearch.action.support.ActionFilters; @@ -41,16 +40,12 @@ import org.opensearch.client.node.NodeClient; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.node.DiscoveryNodes; -import org.opensearch.common.io.stream.StreamInput; -import org.opensearch.common.io.stream.Writeable.Reader; import org.opensearch.common.settings.ClusterSettings; import org.opensearch.common.settings.IndexScopedSettings; import org.opensearch.common.settings.Settings; import org.opensearch.common.settings.SettingsFilter; import org.opensearch.common.settings.SettingsModule; import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.extensions.action.ExtensionAction; -import org.opensearch.extensions.action.ExtensionTransportAction; import org.opensearch.identity.IdentityService; import org.opensearch.plugins.ActionPlugin; import org.opensearch.plugins.ActionPlugin.ActionHandler; @@ -69,9 +64,7 @@ import org.opensearch.usage.UsageService; import java.io.IOException; -import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.function.Supplier; import static java.util.Collections.emptyList; @@ -274,72 +267,4 @@ public List routes() { threadPool.shutdown(); } } - - public void testDynamicActionRegistry() { - ActionFilters emptyFilters = new ActionFilters(Collections.emptySet()); - Map testMap = Map.of(TestAction.INSTANCE, new TestTransportAction("test-action", emptyFilters, null)); - - DynamicActionRegistry dynamicActionRegistry = new DynamicActionRegistry(); - dynamicActionRegistry.registerUnmodifiableActionMap(testMap); - - // Should contain the immutable map entry - assertNotNull(dynamicActionRegistry.get(TestAction.INSTANCE)); - // Should not contain anything not added - assertNull(dynamicActionRegistry.get(MainAction.INSTANCE)); - - // ExtensionsAction not yet registered - ExtensionAction testExtensionAction = new ExtensionAction("extensionId", "actionName"); - ExtensionTransportAction testExtensionTransportAction = new ExtensionTransportAction("test-action", emptyFilters, null, null); - assertNull(dynamicActionRegistry.get(testExtensionAction)); - - // Register an extension action - // Should insert without problem - try { - dynamicActionRegistry.registerDynamicAction(testExtensionAction, testExtensionTransportAction); - } catch (Exception e) { - fail("Should not have thrown exception registering action: " + e); - } - assertEquals(testExtensionTransportAction, dynamicActionRegistry.get(testExtensionAction)); - - // Should fail inserting twice - IllegalArgumentException ex = assertThrows( - IllegalArgumentException.class, - () -> dynamicActionRegistry.registerDynamicAction(testExtensionAction, testExtensionTransportAction) - ); - assertEquals("action [actionName] already registered", ex.getMessage()); - // Should remove without problem - try { - dynamicActionRegistry.unregisterDynamicAction(testExtensionAction); - } catch (Exception e) { - fail("Should not have thrown exception unregistering action: " + e); - } - // Should have been removed - assertNull(dynamicActionRegistry.get(testExtensionAction)); - - // Should fail removing twice - ex = assertThrows(IllegalArgumentException.class, () -> dynamicActionRegistry.unregisterDynamicAction(testExtensionAction)); - assertEquals("action [actionName] was not registered", ex.getMessage()); - } - - private static final class TestAction extends ActionType { - public static final TestAction INSTANCE = new TestAction(); - - private TestAction() { - super("test-action", new Reader() { - @Override - public ActionResponse read(StreamInput in) throws IOException { - return null; - } - }); - } - }; - - private static final class TestTransportAction extends TransportAction { - protected TestTransportAction(String actionName, ActionFilters actionFilters, TaskManager taskManager) { - super(actionName, actionFilters, taskManager); - } - - @Override - protected void doExecute(Task task, ActionRequest request, ActionListener listener) {} - } } diff --git a/server/src/test/java/org/opensearch/action/DynamicActionRegistryTests.java b/server/src/test/java/org/opensearch/action/DynamicActionRegistryTests.java new file mode 100644 index 0000000000000..a5b4f91ff1ed5 --- /dev/null +++ b/server/src/test/java/org/opensearch/action/DynamicActionRegistryTests.java @@ -0,0 +1,131 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.action; + +import org.opensearch.action.ActionModule.DynamicActionRegistry; +import org.opensearch.action.main.MainAction; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.TransportAction; +import org.opensearch.common.io.stream.StreamInput; +import org.opensearch.common.io.stream.Writeable; +import org.opensearch.extensions.action.ExtensionAction; +import org.opensearch.extensions.action.ExtensionTransportAction; +import org.opensearch.rest.NamedRoute; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.extensions.RestSendToExtensionAction; +import org.opensearch.tasks.Task; +import org.opensearch.tasks.TaskManager; +import org.opensearch.test.OpenSearchTestCase; + +import java.io.IOException; +import java.util.Collections; +import java.util.Map; + +import static org.mockito.Mockito.mock; + +public class DynamicActionRegistryTests extends OpenSearchTestCase { + + public void testDynamicActionRegistry() { + ActionFilters emptyFilters = new ActionFilters(Collections.emptySet()); + Map testMap = Map.of(TestAction.INSTANCE, new TestTransportAction("test-action", emptyFilters, null)); + + DynamicActionRegistry dynamicActionRegistry = new DynamicActionRegistry(); + dynamicActionRegistry.registerUnmodifiableActionMap(testMap); + + // Should contain the immutable map entry + assertNotNull(dynamicActionRegistry.get(TestAction.INSTANCE)); + // Should not contain anything not added + assertNull(dynamicActionRegistry.get(MainAction.INSTANCE)); + + // ExtensionsAction not yet registered + ExtensionAction testExtensionAction = new ExtensionAction("extensionId", "actionName"); + ExtensionTransportAction testExtensionTransportAction = new ExtensionTransportAction("test-action", emptyFilters, null, null); + assertNull(dynamicActionRegistry.get(testExtensionAction)); + + // Register an extension action + // Should insert without problem + try { + dynamicActionRegistry.registerDynamicAction(testExtensionAction, testExtensionTransportAction); + } catch (Exception e) { + fail("Should not have thrown exception registering action: " + e); + } + assertEquals(testExtensionTransportAction, dynamicActionRegistry.get(testExtensionAction)); + + // Should fail inserting twice + IllegalArgumentException ex = assertThrows( + IllegalArgumentException.class, + () -> dynamicActionRegistry.registerDynamicAction(testExtensionAction, testExtensionTransportAction) + ); + assertEquals("action [actionName] already registered", ex.getMessage()); + // Should remove without problem + try { + dynamicActionRegistry.unregisterDynamicAction(testExtensionAction); + } catch (Exception e) { + fail("Should not have thrown exception unregistering action: " + e); + } + // Should have been removed + assertNull(dynamicActionRegistry.get(testExtensionAction)); + + // Should fail removing twice + ex = assertThrows(IllegalArgumentException.class, () -> dynamicActionRegistry.unregisterDynamicAction(testExtensionAction)); + assertEquals("action [actionName] was not registered", ex.getMessage()); + } + + public void testDynamicActionRegistryWithNamedRoutes() { + RestSendToExtensionAction action = mock(RestSendToExtensionAction.class); + RestSendToExtensionAction action2 = mock(RestSendToExtensionAction.class); + NamedRoute r1 = new NamedRoute(RestRequest.Method.GET, "/foo", "foo"); + NamedRoute r2 = new NamedRoute(RestRequest.Method.GET, "/bar", "bar"); + + DynamicActionRegistry registry = new DynamicActionRegistry(); + registry.registerDynamicRoute(r1, action); + registry.registerDynamicRoute(r2, action2); + + assertTrue(registry.isActionRegistered("foo")); + assertTrue(registry.isActionRegistered("bar")); + } + + public void testDynamicActionRegistryRegisterAndUnregisterWithNamedRoutes() { + RestSendToExtensionAction action = mock(RestSendToExtensionAction.class); + RestSendToExtensionAction action2 = mock(RestSendToExtensionAction.class); + NamedRoute r1 = new NamedRoute(RestRequest.Method.GET, "/foo", "foo"); + NamedRoute r2 = new NamedRoute(RestRequest.Method.GET, "/bar", "bar"); + + DynamicActionRegistry registry = new DynamicActionRegistry(); + registry.registerDynamicRoute(r1, action); + registry.registerDynamicRoute(r2, action2); + + registry.unregisterDynamicRoute(r2); + + assertTrue(registry.isActionRegistered("foo")); + assertFalse(registry.isActionRegistered("bar")); + } + + private static final class TestAction extends ActionType { + public static final TestAction INSTANCE = new TestAction(); + + private TestAction() { + super("test-action", new Writeable.Reader() { + @Override + public ActionResponse read(StreamInput in) throws IOException { + return null; + } + }); + } + }; + + private static final class TestTransportAction extends TransportAction { + protected TestTransportAction(String actionName, ActionFilters actionFilters, TaskManager taskManager) { + super(actionName, actionFilters, taskManager); + } + + @Override + protected void doExecute(Task task, ActionRequest request, ActionListener listener) {} + } +} diff --git a/server/src/test/java/org/opensearch/extensions/ExtensionsManagerTests.java b/server/src/test/java/org/opensearch/extensions/ExtensionsManagerTests.java index 7bee57bb2beec..392e7e02ebbcc 100644 --- a/server/src/test/java/org/opensearch/extensions/ExtensionsManagerTests.java +++ b/server/src/test/java/org/opensearch/extensions/ExtensionsManagerTests.java @@ -41,6 +41,7 @@ import org.junit.Before; import org.opensearch.Version; import org.opensearch.action.ActionModule; +import org.opensearch.action.ActionModule.DynamicActionRegistry; import org.opensearch.action.admin.cluster.state.ClusterStateResponse; import org.opensearch.client.node.NodeClient; import org.opensearch.cluster.ClusterSettingsResponse; @@ -96,6 +97,7 @@ public class ExtensionsManagerTests extends OpenSearchTestCase { private TransportService transportService; private ActionModule actionModule; + private DynamicActionRegistry dynamicActionRegistry; private RestController restController; private SettingsModule settingsModule; private ClusterService clusterService; @@ -160,6 +162,7 @@ public void setup() throws Exception { Collections.emptySet() ); actionModule = mock(ActionModule.class); + dynamicActionRegistry = mock(DynamicActionRegistry.class); restController = new RestController( emptySet(), null, @@ -168,6 +171,7 @@ public void setup() throws Exception { new UsageService(), new IdentityService(Settings.EMPTY, List.of()) ); + when(actionModule.getDynamicActionRegistry()).thenReturn(mock(DynamicActionRegistry.class)); when(actionModule.getRestController()).thenReturn(restController); settingsModule = new SettingsModule(Settings.EMPTY, emptyList(), emptyList(), emptySet()); clusterService = createClusterService(threadPool); @@ -474,7 +478,7 @@ public void testHandleRegisterRestActionsRequest() throws Exception { List deprecatedActionsList = List.of("GET /deprecated/foo", "It's deprecated!"); RegisterRestActionsRequest registerActionsRequest = new RegisterRestActionsRequest(uniqueIdStr, actionsList, deprecatedActionsList); TransportResponse response = extensionsManager.getRestActionsRequestHandler() - .handleRegisterRestActionsRequest(registerActionsRequest); + .handleRegisterRestActionsRequest(registerActionsRequest, actionModule.getDynamicActionRegistry()); assertEquals(AcknowledgedResponse.class, response.getClass()); assertTrue(((AcknowledgedResponse) response).getStatus()); } @@ -506,7 +510,8 @@ public void testHandleRegisterRestActionsRequestWithInvalidMethod() throws Excep RegisterRestActionsRequest registerActionsRequest = new RegisterRestActionsRequest(uniqueIdStr, actionsList, deprecatedActionsList); expectThrows( IllegalArgumentException.class, - () -> extensionsManager.getRestActionsRequestHandler().handleRegisterRestActionsRequest(registerActionsRequest) + () -> extensionsManager.getRestActionsRequestHandler() + .handleRegisterRestActionsRequest(registerActionsRequest, actionModule.getDynamicActionRegistry()) ); } @@ -520,7 +525,8 @@ public void testHandleRegisterRestActionsRequestWithInvalidDeprecatedMethod() th RegisterRestActionsRequest registerActionsRequest = new RegisterRestActionsRequest(uniqueIdStr, actionsList, deprecatedActionsList); expectThrows( IllegalArgumentException.class, - () -> extensionsManager.getRestActionsRequestHandler().handleRegisterRestActionsRequest(registerActionsRequest) + () -> extensionsManager.getRestActionsRequestHandler() + .handleRegisterRestActionsRequest(registerActionsRequest, actionModule.getDynamicActionRegistry()) ); } @@ -533,7 +539,8 @@ public void testHandleRegisterRestActionsRequestWithInvalidUri() throws Exceptio RegisterRestActionsRequest registerActionsRequest = new RegisterRestActionsRequest(uniqueIdStr, actionsList, deprecatedActionsList); expectThrows( IllegalArgumentException.class, - () -> extensionsManager.getRestActionsRequestHandler().handleRegisterRestActionsRequest(registerActionsRequest) + () -> extensionsManager.getRestActionsRequestHandler() + .handleRegisterRestActionsRequest(registerActionsRequest, dynamicActionRegistry) ); } @@ -546,7 +553,8 @@ public void testHandleRegisterRestActionsRequestWithInvalidDeprecatedUri() throw RegisterRestActionsRequest registerActionsRequest = new RegisterRestActionsRequest(uniqueIdStr, actionsList, deprecatedActionsList); expectThrows( IllegalArgumentException.class, - () -> extensionsManager.getRestActionsRequestHandler().handleRegisterRestActionsRequest(registerActionsRequest) + () -> extensionsManager.getRestActionsRequestHandler() + .handleRegisterRestActionsRequest(registerActionsRequest, dynamicActionRegistry) ); } diff --git a/server/src/test/java/org/opensearch/extensions/rest/RestSendToExtensionActionTests.java b/server/src/test/java/org/opensearch/extensions/rest/RestSendToExtensionActionTests.java index b98b4955ff75e..df047afb677d9 100644 --- a/server/src/test/java/org/opensearch/extensions/rest/RestSendToExtensionActionTests.java +++ b/server/src/test/java/org/opensearch/extensions/rest/RestSendToExtensionActionTests.java @@ -19,34 +19,52 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.emptySet; +import static org.mockito.Mockito.mock; import org.junit.After; import org.junit.Before; import org.opensearch.Version; +import org.opensearch.action.ActionModule; +import org.opensearch.action.ActionModule.DynamicActionRegistry; +import org.opensearch.action.admin.cluster.health.ClusterHealthAction; +import org.opensearch.action.admin.cluster.health.TransportClusterHealthAction; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.node.DiscoveryNode; import org.opensearch.common.io.stream.NamedWriteableRegistry; import org.opensearch.common.network.NetworkService; import org.opensearch.common.settings.Settings; +import org.opensearch.common.settings.SettingsModule; import org.opensearch.common.transport.TransportAddress; import org.opensearch.common.util.PageCacheRecycler; +import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.extensions.DiscoveryExtensionNode; +import org.opensearch.extensions.action.ExtensionAction; +import org.opensearch.extensions.action.ExtensionTransportAction; +import org.opensearch.identity.IdentityService; import org.opensearch.indices.breaker.NoneCircuitBreakerService; +import org.opensearch.rest.NamedRoute; import org.opensearch.rest.RestHandler.Route; import org.opensearch.rest.RestRequest.Method; +import org.opensearch.rest.extensions.RestSendToExtensionAction; import org.opensearch.test.OpenSearchTestCase; import org.opensearch.test.transport.MockTransportService; import org.opensearch.threadpool.TestThreadPool; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; import org.opensearch.transport.nio.MockNioTransport; +import org.opensearch.usage.UsageService; public class RestSendToExtensionActionTests extends OpenSearchTestCase { private TransportService transportService; private MockNioTransport transport; private DiscoveryExtensionNode discoveryExtensionNode; + private ActionModule actionModule; + private DynamicActionRegistry dynamicActionRegistry; private final ThreadPool threadPool = new TestThreadPool(RestSendToExtensionActionTests.class.getSimpleName()); @Before @@ -86,6 +104,23 @@ public void setup() throws Exception { Version.fromString("3.0.0"), Collections.emptyList() ); + SettingsModule settingsModule = new SettingsModule(settings); + UsageService usageService = new UsageService(); + actionModule = new ActionModule( + settingsModule.getSettings(), + new IndexNameExpressionResolver(new ThreadContext(Settings.EMPTY)), + settingsModule.getIndexScopedSettings(), + settingsModule.getClusterSettings(), + settingsModule.getSettingsFilter(), + mock(ThreadPool.class), + emptyList(), + null, + null, + usageService, + null, + new IdentityService(Settings.EMPTY, new ArrayList<>()) + ); + dynamicActionRegistry = actionModule.getDynamicActionRegistry(); } @Override @@ -105,7 +140,8 @@ public void testRestSendToExtensionAction() throws Exception { RestSendToExtensionAction restSendToExtensionAction = new RestSendToExtensionAction( registerRestActionRequest, discoveryExtensionNode, - transportService + transportService, + dynamicActionRegistry ); assertEquals("send_to_extension_action", restSendToExtensionAction.getName()); @@ -127,6 +163,124 @@ public void testRestSendToExtensionAction() throws Exception { assertTrue(expectedMethods.containsAll(methods)); } + public void testRestSendToExtensionActionWithNamedRoute() throws Exception { + RegisterRestActionsRequest registerRestActionRequest = new RegisterRestActionsRequest( + "uniqueid1", + List.of("GET /foo foo", "PUT /bar bar", "POST /baz baz"), + List.of("GET /deprecated/foo foo_deprecated", "It's deprecated!") + ); + RestSendToExtensionAction restSendToExtensionAction = new RestSendToExtensionAction( + registerRestActionRequest, + discoveryExtensionNode, + transportService, + dynamicActionRegistry + ); + + assertEquals("send_to_extension_action", restSendToExtensionAction.getName()); + List expected = new ArrayList<>(); + String uriPrefix = "/_extensions/_uniqueid1"; + expected.add(new NamedRoute(Method.GET, uriPrefix + "/foo", "foo")); + expected.add(new NamedRoute(Method.PUT, uriPrefix + "/bar", "bar")); + expected.add(new NamedRoute(Method.POST, uriPrefix + "/baz", "baz")); + + List routes = restSendToExtensionAction.routes(); + assertEquals(expected.size(), routes.size()); + List expectedPaths = expected.stream().map(Route::getPath).collect(Collectors.toList()); + List paths = routes.stream().map(Route::getPath).collect(Collectors.toList()); + List expectedMethods = expected.stream().map(Route::getMethod).collect(Collectors.toList()); + List methods = routes.stream().map(Route::getMethod).collect(Collectors.toList()); + List expectedNames = expected.stream().map(NamedRoute::name).collect(Collectors.toList()); + List names = routes.stream().map(r -> ((NamedRoute) r).name()).collect(Collectors.toList()); + assertTrue(paths.containsAll(expectedPaths)); + assertTrue(expectedPaths.containsAll(paths)); + assertTrue(methods.containsAll(expectedMethods)); + assertTrue(expectedMethods.containsAll(methods)); + assertTrue(expectedNames.containsAll(names)); + } + + public void testRestSendToExtensionMultipleNamedRoutesWithSameName() throws Exception { + RegisterRestActionsRequest registerRestActionRequest = new RegisterRestActionsRequest( + "uniqueid1", + List.of("GET /foo foo", "PUT /bar foo"), + List.of() + ); + expectThrows( + IllegalArgumentException.class, + () -> new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService, dynamicActionRegistry) + ); + } + + public void testRestSendToExtensionMultipleRoutesWithSameMethodAndPath() throws Exception { + RegisterRestActionsRequest registerRestActionRequest = new RegisterRestActionsRequest( + "uniqueid1", + List.of("GET /foo", "GET /foo"), + List.of() + ); + expectThrows( + IllegalArgumentException.class, + () -> new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService, dynamicActionRegistry) + ); + } + + public void testRestSendToExtensionMultipleRoutesWithSameMethodAndPathWithDifferentPathParams() throws Exception { + RegisterRestActionsRequest registerRestActionRequest = new RegisterRestActionsRequest( + "uniqueid1", + List.of("GET /foo/{path_param1}", "GET /foo/{path_param2}"), + List.of() + ); + expectThrows( + IllegalArgumentException.class, + () -> new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService, dynamicActionRegistry) + ); + } + + public void testRestSendToExtensionMultipleRoutesWithSameMethodAndPathWithPathParams() throws Exception { + RegisterRestActionsRequest registerRestActionRequest = new RegisterRestActionsRequest( + "uniqueid1", + List.of("GET /foo/{path_param}", "GET /foo/{path_param}/list"), + List.of() + ); + try { + new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService, dynamicActionRegistry); + } catch (IllegalArgumentException e) { + fail("IllegalArgumentException should not be thrown for different paths"); + } + } + + public void testRestSendToExtensionWithNamedRouteCollidingWithDynamicTransportAction() throws Exception { + DynamicActionRegistry dynamicActionRegistry = actionModule.getDynamicActionRegistry(); + ActionFilters emptyFilters = new ActionFilters(Collections.emptySet()); + ExtensionAction testExtensionAction = new ExtensionAction("extensionId", "test:action/name"); + ExtensionTransportAction testExtensionTransportAction = new ExtensionTransportAction("test:action/name", emptyFilters, null, null); + assertNull(dynamicActionRegistry.get(testExtensionAction)); + dynamicActionRegistry.registerDynamicAction(testExtensionAction, testExtensionTransportAction); + + RegisterRestActionsRequest registerRestActionRequest = new RegisterRestActionsRequest( + "uniqueid1", + List.of("GET /foo test:action/name"), + List.of() + ); + + expectThrows( + IllegalArgumentException.class, + () -> new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService, dynamicActionRegistry) + ); + } + + public void testRestSendToExtensionWithNamedRouteCollidingWithNativeTransportAction() throws Exception { + actionModule.getDynamicActionRegistry() + .registerUnmodifiableActionMap(Map.of(ClusterHealthAction.INSTANCE, mock(TransportClusterHealthAction.class))); + RegisterRestActionsRequest registerRestActionRequest = new RegisterRestActionsRequest( + "uniqueid1", + List.of("GET /foo " + ClusterHealthAction.NAME), + List.of() + ); + expectThrows( + IllegalArgumentException.class, + () -> new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService, dynamicActionRegistry) + ); + } + public void testRestSendToExtensionActionFilterHeaders() throws Exception { RegisterRestActionsRequest registerRestActionRequest = new RegisterRestActionsRequest( "uniqueid1", @@ -136,7 +290,8 @@ public void testRestSendToExtensionActionFilterHeaders() throws Exception { RestSendToExtensionAction restSendToExtensionAction = new RestSendToExtensionAction( registerRestActionRequest, discoveryExtensionNode, - transportService + transportService, + dynamicActionRegistry ); Map> headers = new HashMap<>(); @@ -162,7 +317,7 @@ public void testRestSendToExtensionActionBadMethod() throws Exception { ); expectThrows( IllegalArgumentException.class, - () -> new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService) + () -> new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService, dynamicActionRegistry) ); } @@ -174,7 +329,7 @@ public void testRestSendToExtensionActionBadDeprecatedMethod() throws Exception ); expectThrows( IllegalArgumentException.class, - () -> new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService) + () -> new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService, dynamicActionRegistry) ); } @@ -186,7 +341,7 @@ public void testRestSendToExtensionActionMissingUri() throws Exception { ); expectThrows( IllegalArgumentException.class, - () -> new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService) + () -> new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService, dynamicActionRegistry) ); } @@ -198,7 +353,7 @@ public void testRestSendToExtensionActionMissingDeprecatedUri() throws Exception ); expectThrows( IllegalArgumentException.class, - () -> new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService) + () -> new RestSendToExtensionAction(registerRestActionRequest, discoveryExtensionNode, transportService, dynamicActionRegistry) ); } } diff --git a/server/src/test/java/org/opensearch/extensions/rest/RouteHandlerTests.java b/server/src/test/java/org/opensearch/extensions/rest/RouteHandlerTests.java new file mode 100644 index 0000000000000..855296b2038f0 --- /dev/null +++ b/server/src/test/java/org/opensearch/extensions/rest/RouteHandlerTests.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.extensions.rest; + +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestStatus; +import org.opensearch.test.OpenSearchTestCase; + +public class RouteHandlerTests extends OpenSearchTestCase { + public void testUnnamedRouteHandler() { + RouteHandler rh = new RouteHandler( + RestRequest.Method.GET, + "/foo/bar", + req -> new ExtensionRestResponse(req, RestStatus.OK, "content") + ); + + assertEquals(null, rh.name()); + } + + public void testNamedRouteHandler() { + RouteHandler rh = new RouteHandler( + "foo", + RestRequest.Method.GET, + "/foo/bar", + req -> new ExtensionRestResponse(req, RestStatus.OK, "content") + ); + + assertEquals("foo", rh.name()); + } +} diff --git a/server/src/test/java/org/opensearch/rest/NamedRouteTests.java b/server/src/test/java/org/opensearch/rest/NamedRouteTests.java new file mode 100644 index 0000000000000..d489321ea5dc6 --- /dev/null +++ b/server/src/test/java/org/opensearch/rest/NamedRouteTests.java @@ -0,0 +1,95 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.rest; + +import org.opensearch.OpenSearchException; +import org.opensearch.test.OpenSearchTestCase; + +import static org.opensearch.rest.NamedRoute.MAX_LENGTH_OF_ACTION_NAME; + +public class NamedRouteTests extends OpenSearchTestCase { + + public void testNamedRouteWithNullName() { + try { + NamedRoute r = new NamedRoute(RestRequest.Method.GET, "foo/bar", null); + fail("Expected NamedRoute to throw exception on null name provided"); + } catch (OpenSearchException e) { + assertTrue(e.getMessage().contains("Invalid route name specified")); + } + } + + public void testNamedRouteWithEmptyName() { + try { + NamedRoute r = new NamedRoute(RestRequest.Method.GET, "foo/bar", ""); + fail("Expected NamedRoute to throw exception on empty name provided"); + } catch (OpenSearchException e) { + assertTrue(e.getMessage().contains("Invalid route name specified")); + } + } + + public void testNamedRouteWithNameContainingSpace() { + try { + NamedRoute r = new NamedRoute(RestRequest.Method.GET, "foo/bar", "foo bar"); + fail("Expected NamedRoute to throw exception on name containing space name provided"); + } catch (OpenSearchException e) { + assertTrue(e.getMessage().contains("Invalid route name specified")); + } + } + + public void testNamedRouteWithNameContainingInvalidCharacters() { + try { + NamedRoute r = new NamedRoute(RestRequest.Method.GET, "foo/bar", "foo@bar!"); + fail("Expected NamedRoute to throw exception on name containing invalid characters name provided"); + } catch (OpenSearchException e) { + assertTrue(e.getMessage().contains("Invalid route name specified")); + } + } + + public void testNamedRouteWithNameOverMaximumLength() { + try { + String repeated = new String(new char[MAX_LENGTH_OF_ACTION_NAME + 1]).replace("\0", "x"); + NamedRoute r = new NamedRoute(RestRequest.Method.GET, "foo/bar", repeated); + fail("Expected NamedRoute to throw exception on name over maximum length supplied"); + } catch (OpenSearchException e) { + assertTrue(e.getMessage().contains("Invalid route name specified")); + } + } + + public void testNamedRouteWithValidActionName() { + try { + NamedRoute r = new NamedRoute(RestRequest.Method.GET, "foo/bar", "foo:bar"); + } catch (OpenSearchException e) { + fail("Did not expect NamedRoute to throw exception on valid action name"); + } + } + + public void testNamedRouteWithValidActionNameWithForwardSlash() { + try { + NamedRoute r = new NamedRoute(RestRequest.Method.GET, "foo/bar", "foo:bar/baz"); + } catch (OpenSearchException e) { + fail("Did not expect NamedRoute to throw exception on valid action name"); + } + } + + public void testNamedRouteWithValidActionNameWithWildcard() { + try { + NamedRoute r = new NamedRoute(RestRequest.Method.GET, "foo/bar", "foo:bar/*"); + } catch (OpenSearchException e) { + fail("Did not expect NamedRoute to throw exception on valid action name"); + } + } + + public void testNamedRouteWithValidActionNameWithUnderscore() { + try { + NamedRoute r = new NamedRoute(RestRequest.Method.GET, "foo/bar", "foo:bar_baz"); + } catch (OpenSearchException e) { + fail("Did not expect NamedRoute to throw exception on valid action name"); + } + } +}