Skip to content

Commit 276c7a8

Browse files
authored
Merge pull request #177 from networknt/176-add-regression-test-coverage-for-lambdaproxymiddleware-route-miss
Add Regression Test Coverage For LambdaProxyMiddleware Route Miss
2 parents e91e37c + bd54fe9 commit 276c7a8

3 files changed

Lines changed: 131 additions & 5 deletions

File tree

src/main/java/com/networknt/aws/lambda/handler/middleware/proxy/LambdaProxyMiddleware.java

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ public LambdaProxyMiddleware() {
4949
LOG.info("LambdaProxyMiddleware is constructed");
5050
}
5151

52+
/**
53+
* Builds only the route table from the supplied function mapping, without creating an AWS Lambda
54+
* client. Visible for testing so that the routing behaviour can be exercised without AWS access.
55+
*
56+
* @param functions the endpoint to Lambda function mapping, keyed by {@code path@method}
57+
*/
58+
LambdaProxyMiddleware(final Map<String, String> functions) {
59+
this.config = null;
60+
this.client = null;
61+
populateMethodToMatcherMap(functions);
62+
}
63+
5264
private LambdaAsyncClient initClient(LambdaProxyConfig config) {
5365
/* Create our netty client */
5466
var readTimeout = config.getReadTimeout();
@@ -94,13 +106,11 @@ public Status execute(LightLambdaExchange exchange) {
94106
var path = exchange.getRequest().getPath();
95107
var method = exchange.getRequest().getHttpMethod().toLowerCase();
96108
LOG.debug("Request path: {} -- Request method: {} -- Start time: {}", path, method, System.currentTimeMillis());
97-
PathTemplateMatcher<String> matcher = methodToMatcherMap.get(method);
98-
PathTemplateMatcher.PathMatchResult<String> result = matcher == null ? null : matcher.match(path);
99-
if (result == null) {
109+
var functionName = resolveFunctionName(path, method);
110+
if (functionName == null) {
100111
LOG.error("No lambda function found for path: {} and method: {}", path, method);
101112
return new Status(FAILED_TO_INVOKE_LAMBDA, path + "@" + method);
102113
}
103-
var functionName = result.getValue();
104114
LOG.trace("Function name: {}", functionName);
105115
var res = this.invokeFunction(this.client, functionName, exchange);
106116
if (res == null) {
@@ -122,6 +132,25 @@ public Status execute(LightLambdaExchange exchange) {
122132
}
123133
}
124134

135+
/**
136+
* Resolves the Lambda function configured for the given request path and HTTP method.
137+
* <p>
138+
* Returns {@code null} both when no matcher is registered for the method and when the method has a
139+
* matcher but no template matches the path, so that the caller can treat every routing miss the
140+
* same way.
141+
*
142+
* @param path the request path
143+
* @param method the request HTTP method, already normalized to lower case
144+
* @return the configured Lambda function name, or {@code null} when the request does not route
145+
*/
146+
String resolveFunctionName(final String path, final String method) {
147+
PathTemplateMatcher<String> matcher = this.methodToMatcherMap.get(method);
148+
if (matcher == null)
149+
return null;
150+
PathTemplateMatcher.PathMatchResult<String> result = matcher.match(path);
151+
return result == null ? null : result.getValue();
152+
}
153+
125154
private void populateMethodToMatcherMap(final Map<String, String> functions) {
126155
this.methodToMatcherMap.clear();
127156
for (var entry : functions.entrySet()) {
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package com.networknt.aws.lambda.handler.middleware.proxy;
2+
3+
import com.amazonaws.services.lambda.runtime.Context;
4+
import com.networknt.aws.lambda.InvocationResponse;
5+
import com.networknt.aws.lambda.LambdaContext;
6+
import com.networknt.aws.lambda.LightLambdaExchange;
7+
import com.networknt.aws.lambda.TestUtils;
8+
import com.networknt.status.Status;
9+
import org.junit.jupiter.api.Assertions;
10+
import org.junit.jupiter.api.Test;
11+
12+
import java.util.Map;
13+
14+
/**
15+
* Routing tests for {@link LambdaProxyMiddleware}.
16+
* <p>
17+
* These exercise the real middleware rather than {@code MockLambdaProxyMiddleware}, which resolves
18+
* functions through a plain map lookup and therefore cannot cover the matcher logic at all.
19+
*/
20+
public class LambdaProxyMiddlewareTest {
21+
22+
private static final Map<String, String> FUNCTIONS = Map.of(
23+
"/v1/pets@get", "PetsGetFunction",
24+
"/v1/pets@post", "PetsPostFunction",
25+
"/v1/pets/{petId}@get", "PetsPetIdGetFunction");
26+
27+
private static LightLambdaExchange exchangeFor(final String path, final String method) {
28+
var requestEvent = TestUtils.createTestRequestEvent();
29+
requestEvent.setPath(path);
30+
requestEvent.setHttpMethod(method);
31+
32+
InvocationResponse invocation = InvocationResponse.builder()
33+
.requestId("12345")
34+
.event(requestEvent)
35+
.build();
36+
37+
Context lambdaContext = new LambdaContext(invocation.getRequestId());
38+
final var exchange = new LightLambdaExchange(lambdaContext, null);
39+
exchange.setInitialRequest(requestEvent);
40+
return exchange;
41+
}
42+
43+
@Test
44+
public void testResolveFunctionNameMatchesConfiguredRoute() {
45+
var middleware = new LambdaProxyMiddleware(FUNCTIONS);
46+
Assertions.assertEquals("PetsGetFunction", middleware.resolveFunctionName("/v1/pets", "get"));
47+
Assertions.assertEquals("PetsPostFunction", middleware.resolveFunctionName("/v1/pets", "post"));
48+
}
49+
50+
@Test
51+
public void testResolveFunctionNameMatchesPathTemplate() {
52+
var middleware = new LambdaProxyMiddleware(FUNCTIONS);
53+
Assertions.assertEquals("PetsPetIdGetFunction", middleware.resolveFunctionName("/v1/pets/123", "get"));
54+
}
55+
56+
/**
57+
* Regression test for the NPE reported in issue #173. The path matches a configured function but
58+
* no matcher is registered for the method, which used to dereference a null matcher.
59+
*/
60+
@Test
61+
public void testResolveFunctionNameReturnsNullWhenMethodHasNoMatcher() {
62+
var middleware = new LambdaProxyMiddleware(FUNCTIONS);
63+
Assertions.assertNull(middleware.resolveFunctionName("/v1/pets", "delete"));
64+
}
65+
66+
@Test
67+
public void testResolveFunctionNameReturnsNullWhenPathDoesNotMatch() {
68+
var middleware = new LambdaProxyMiddleware(FUNCTIONS);
69+
Assertions.assertNull(middleware.resolveFunctionName("/v1/unknown", "get"));
70+
}
71+
72+
/**
73+
* Regression test for issue #173 at the middleware boundary. An unmapped method must produce a
74+
* routing failure status rather than throwing.
75+
*/
76+
@Test
77+
public void testExecuteReturnsFailureStatusWhenMethodHasNoMatcher() {
78+
var middleware = new LambdaProxyMiddleware(FUNCTIONS);
79+
Status status = Assertions.assertDoesNotThrow(
80+
() -> middleware.execute(exchangeFor("/v1/pets", "DELETE")));
81+
Assertions.assertEquals(LambdaProxyMiddleware.FAILED_TO_INVOKE_LAMBDA, status.getCode());
82+
Assertions.assertTrue(status.getDescription().contains("/v1/pets@delete"),
83+
"expected the failure description to identify the unroutable endpoint, but was: "
84+
+ status.getDescription());
85+
}
86+
87+
@Test
88+
public void testExecuteReturnsFailureStatusWhenPathDoesNotMatch() {
89+
var middleware = new LambdaProxyMiddleware(FUNCTIONS);
90+
Status status = Assertions.assertDoesNotThrow(
91+
() -> middleware.execute(exchangeFor("/v1/unknown", "GET")));
92+
Assertions.assertEquals(LambdaProxyMiddleware.FAILED_TO_INVOKE_LAMBDA, status.getCode());
93+
}
94+
}

src/test/java/com/networknt/aws/lambda/middleware/proxy/MockLambdaProxyMiddlewareTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ public void testConstructor() {
1212
MockLambdaProxyMiddleware mockLambdaProxyMiddleware = new MockLambdaProxyMiddleware();
1313
Map<String, PathTemplateMatcher<String>> methodToMatcherMap = MockLambdaProxyMiddleware.methodToMatcherMap;
1414
Assertions.assertFalse(methodToMatcherMap.isEmpty());
15-
PathTemplateMatcher.PathMatchResult<String> result = methodToMatcherMap.get("get").match("/v1/pets/123");
15+
PathTemplateMatcher<String> matcher = methodToMatcherMap.get("get");
16+
Assertions.assertNotNull(matcher, "no matcher registered for the 'get' method");
17+
PathTemplateMatcher.PathMatchResult<String> result = matcher.match("/v1/pets/123");
18+
Assertions.assertNotNull(result, "no path template matched /v1/pets/123");
1619
Assertions.assertEquals("PetsPetIdGetFunction", result.getValue());
1720
}
1821
}

0 commit comments

Comments
 (0)