package org.yamcs.web.rest; import static io.netty.handler.codec.http.HttpMethod.DELETE; import static io.netty.handler.codec.http.HttpMethod.GET; import static io.netty.handler.codec.http.HttpMethod.PATCH; import static io.netty.handler.codec.http.HttpMethod.POST; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.lang.invoke.MethodHandleInfo; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodHandles.Lookup; import java.util.List; import java.util.regex.MatchResult; import org.junit.Before; import org.junit.Test; import org.yamcs.web.MethodNotAllowedException; import org.yamcs.web.RouteHandler; import org.yamcs.web.rest.Router.RouteMatch; import io.netty.channel.ChannelFuture; import io.netty.handler.codec.http.HttpMethod; public class RouterTest { private Router router; @Before public void before() { router = new Router(); router.registerRouteHandler(null, new RouteHandler() { // Some simple routes @Route(path = "/a/:adjective/path") public void pathA() {} @Route(path = "/b/:adjective?/path") public void pathB() {} @Route(path = "/c/:adjective*/path") public void pathC() {} // Identical paths, but different HTTP methods @Route(path = "/d/:adjective/path", method = { "POST", "PATCH" }) public void pathM() {} @Route(path = "/d/:adjective/path", method = "GET") public void pathN() {} // Multiple paths, same java method @Route(path = "/e/:adjective/path") @Route(path = "/f/different/path") public void pathS() {} // A more complicated set of rules sharing partial paths. // Route order is in principle non-deterministic, due to java reflection limitations. // Our algorithm is designed to stop on the first match by descending // string length. In addition, we have a flag to indicate priority, but // try to architect paths differently @Route(path = "/g/archive/:instance") public void pathU() {} @Route(path = "/g/archive/:instance/parameters") public void pathV() {} @Route(path = "/g/archive/:instance/parameters/bulk", priority = true) public void pathW() {} @Route(path = "/g/archive/:instance/parameters/:name*") public void pathY() {} @Route(path = "/g/archive/:instance/parameters/:name*/series") public void pathX() {} }); } @Test public void testUnmatchedURI() throws MethodNotAllowedException { assertNull(router.matchURI(GET, "garbage")); } @Test public void testUnmatchedMethod() throws MethodNotAllowedException { assertNotNull(router.matchURI(GET, "/a/great/path")); boolean failed = false; try { assertNull(router.matchURI(POST, "/a/great/path")); } catch (MethodNotAllowedException e) { failed = true; List<HttpMethod> allowedMethods = e.getAllowedMethods(); assertEquals(1, allowedMethods.size()); assertEquals(GET, allowedMethods.get(0)); } assertTrue(failed); } @Test public void testUnmatchedMethod_multipleRoutes() throws MethodNotAllowedException { assertNotNull(router.matchURI(GET, "/d/great/path")); boolean failed = false; try { assertNull(router.matchURI(DELETE, "/d/great/path")); } catch (MethodNotAllowedException e) { failed = true; List<HttpMethod> allowedMethods = e.getAllowedMethods(); assertEquals(3, allowedMethods.size()); // Alphabetic, and combining methods from all routes assertEquals(GET, allowedMethods.get(0)); assertEquals(PATCH, allowedMethods.get(1)); assertEquals(POST, allowedMethods.get(2)); } assertTrue(failed); } @Test public void testSimpleMatch() throws MethodNotAllowedException { MatchResult res = router.matchURI(GET, "/a/great/path").regexMatch; assertEquals("great", res.group(1)); } @Test public void testSimpleOptionalMatch() throws MethodNotAllowedException { MatchResult res = router.matchURI(GET, "/b/fascinating/path").regexMatch; assertEquals("fascinating", res.group(1)); res = router.matchURI(GET, "/b/path").regexMatch; assertEquals(null, res.group(1)); } @Test public void testSimpleStarMatch() throws MethodNotAllowedException { RouteMatch match = router.matchURI(GET, "/c/really/great/fascinating/path"); assertEquals("really/great/fascinating", match.regexMatch.group(1)); match = router.matchURI(GET, "/c/path"); assertNull("Star must match at least one segment", match); } @Test public void testDistinctMethodMatch() throws MethodNotAllowedException { RouteMatch match1 = router.matchURI(GET, "/d/great/path"); MethodHandleInfo info1 = MethodHandles.lookup().revealDirect(match1.routeConfig.handle); assertEquals("pathN", info1.getName()); RouteMatch match2 = router.matchURI(POST, "/d/great/path"); MethodHandleInfo info2 = MethodHandles.lookup().revealDirect(match2.routeConfig.handle); assertEquals("pathM", info2.getName()); } @Test public void testMultipleRouteMatch() throws MethodNotAllowedException { RouteMatch match1 = router.matchURI(GET, "/e/great/path"); RouteMatch match2 = router.matchURI(GET, "/f/different/path"); Lookup lookup = MethodHandles.lookup(); MethodHandleInfo info1 = lookup.revealDirect(match1.routeConfig.handle); MethodHandleInfo info2 = lookup.revealDirect(match2.routeConfig.handle); assertEquals(info1.getName(), info2.getName()); } @Test public void testMultipleRouteMatching() throws MethodNotAllowedException { MatchResult res = router.matchURI(GET, "/g/archive/simulator").regexMatch; assertEquals(1, res.groupCount()); assertEquals("simulator", res.group(1)); res = router.matchURI(GET, "/g/archive/simulator/parameters/YSS/SIMULATOR/BatteryVoltage1").regexMatch; assertEquals(2, res.groupCount()); assertEquals("simulator", res.group(1)); assertEquals("YSS/SIMULATOR/BatteryVoltage1", res.group(2)); res = router.matchURI(GET, "/g/archive/simulator/parameters/bulk").regexMatch; assertEquals(1, res.groupCount()); assertEquals("simulator", res.group(1)); res = router.matchURI(GET, "/g/archive/simulator/parameters/YSS/SIMULATOR/BatteryVoltage1/series").regexMatch; assertEquals(2, res.groupCount()); assertEquals("simulator", res.group(1)); assertEquals("YSS/SIMULATOR/BatteryVoltage1", res.group(2)); } @Test public void testRouteParams() throws MethodNotAllowedException { MockRestRouter router = new MockRestRouter(); router.registerRouteHandler(null, new RouteHandler() { @Route(path = "/h/archive/:bla?/:instance") public ChannelFuture abc(RestRequest req) { return null; } }); RouteMatch match = router.matchURI(GET, "/h/archive/simulator"); MockRestRequest mockRequest = new MockRestRequest(); mockRequest.setRouteMatch(match); router.dispatch(mockRequest, match); Lookup lookup = MethodHandles.lookup(); MethodHandleInfo info = lookup.revealDirect(match.routeConfig.handle); assertEquals("abc", info.getName()); RestRequest observedRestRequest = router.observedRestRequest; assertTrue(observedRestRequest.hasRouteParam("instance")); assertEquals("simulator", observedRestRequest.getRouteParam("instance")); } private static final class MockRestRouter extends Router { RestRequest observedRestRequest; @Override protected void dispatch(RestRequest req, RouteMatch match) { observedRestRequest = req; } } private static final class MockRestRequest extends RestRequest { public MockRestRequest() { super(null, null, null, null); } } }