From ed2cacfdcc69343673003f055772445507825ff2 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Tue, 27 Oct 2026 00:26:08 -0400 Subject: [PATCH 1/7] Adopt the shared Lambda runtime mapper and exercise validation in smoke --- ci/jackson3-source-refs.json | 2 +- .../java/com/networknt/aws/lambda/Main.java | 5 +-- .../Jackson3NativeCompatibilityTest.java | 12 ++---- .../com/networknt/aws/lambda/TestUtils.java | 5 +-- tests/native_runtime_smoke.py | 37 ++++++++++++++++++- 5 files changed, 41 insertions(+), 20 deletions(-) diff --git a/ci/jackson3-source-refs.json b/ci/jackson3-source-refs.json index ad31c77..4607f67 100644 --- a/ci/jackson3-source-refs.json +++ b/ci/jackson3-source-refs.json @@ -2,7 +2,7 @@ "http-client": "5ecee29dbb08d813512f5379c2e1b902d553c5d4", "json-overlay": "e309fd63f95317067c9b13a4bed7ac16727bfea5", "light-4j": "97b2359b92ed3e1d05cac04f2da8760aa954e49d", - "light-aws-lambda": "dddbbc9e7f6f690f8688ceac024728b6a84f4808", + "light-aws-lambda": "9d59c6a74112a15f702cfd76ceb401680ad05518", "light-rest-4j": "a31955945036d9108b7e9bb72167415cc9cfc287", "openapi-parser": "9ae849056e49f7c796fe28bfe6093fbe4847cc5c", "yaml-rule": "c237cc3a25720f43e57bf571e25d5fa06687aa3b" diff --git a/src/main/java/com/networknt/aws/lambda/Main.java b/src/main/java/com/networknt/aws/lambda/Main.java index 276910d..3aa0c7d 100644 --- a/src/main/java/com/networknt/aws/lambda/Main.java +++ b/src/main/java/com/networknt/aws/lambda/Main.java @@ -2,17 +2,14 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.ObjectMapper; -import com.networknt.jackson.JacksonDefaults; import com.networknt.aws.lambda.app.LambdaApp; import java.io.IOException; public class Main { - private static final ObjectMapper OBJECT_MAPPER = JacksonDefaults.builder() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build(); + private static final ObjectMapper OBJECT_MAPPER = LambdaRuntimeMappers.mapper(); public static void main(String[] args) throws IOException { diff --git a/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java b/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java index f0266a3..66baa3e 100644 --- a/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java +++ b/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java @@ -5,20 +5,14 @@ import org.junit.jupiter.api.Test; import tools.jackson.databind.ObjectMapper; -import java.lang.reflect.Field; import static org.junit.jupiter.api.Assertions.*; class Jackson3NativeCompatibilityTest { - private ObjectMapper mainMapper() throws Exception { - Field field = Main.class.getDeclaredField("OBJECT_MAPPER"); - field.setAccessible(true); - return (ObjectMapper) field.get(null); - } @Test void runtimeMapperKeepsPermissiveEventBinding() throws Exception { - APIGatewayProxyRequestEvent event = mainMapper().readValue( + APIGatewayProxyRequestEvent event = LambdaRuntimeMappers.mapper().readValue( "{\"path\":\"/v1/pets\",\"httpMethod\":\"POST\",\"futureField\":true} trailing", APIGatewayProxyRequestEvent.class); assertEquals("/v1/pets", event.getPath()); @@ -27,7 +21,7 @@ void runtimeMapperKeepsPermissiveEventBinding() throws Exception { @Test void responseRetainsJackson2ShapeAndRoundTrips() throws Exception { - ObjectMapper mapper = mainMapper(); + ObjectMapper mapper = LambdaRuntimeMappers.mapper(); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withStatusCode(200).withBody("{\"ok\":true}").withIsBase64Encoded(false); String json = mapper.writeValueAsString(response); @@ -39,6 +33,6 @@ void responseRetainsJackson2ShapeAndRoundTrips() throws Exception { @Test void runtimeMapperRetainsJackson2StreamDepth() throws Exception { String nested = "[".repeat(600) + "0" + "]".repeat(600); - assertDoesNotThrow(() -> mainMapper().readTree(nested)); + assertDoesNotThrow(() -> LambdaRuntimeMappers.mapper().readTree(nested)); } } diff --git a/src/test/java/com/networknt/aws/lambda/TestUtils.java b/src/test/java/com/networknt/aws/lambda/TestUtils.java index e36c1c9..55dc0c4 100644 --- a/src/test/java/com/networknt/aws/lambda/TestUtils.java +++ b/src/test/java/com/networknt/aws/lambda/TestUtils.java @@ -2,9 +2,7 @@ import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import tools.jackson.core.JacksonException; -import tools.jackson.databind.DeserializationFeature; import tools.jackson.databind.ObjectMapper; -import com.networknt.jackson.JacksonDefaults; public class TestUtils { @@ -133,8 +131,7 @@ public class TestUtils { " }\n" + "}"; - private static final ObjectMapper OBJECT_MAPPER = JacksonDefaults.builder() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).build(); + private static final ObjectMapper OBJECT_MAPPER = LambdaRuntimeMappers.mapper(); public static APIGatewayProxyRequestEvent createTestRequestEvent() { APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent; diff --git a/tests/native_runtime_smoke.py b/tests/native_runtime_smoke.py index 74891cf..f6bbd5e 100644 --- a/tests/native_runtime_smoke.py +++ b/tests/native_runtime_smoke.py @@ -11,10 +11,11 @@ import threading -def invoke(binary, handler, expected_endpoint): +def invoke(binary, handler, expected_endpoint, count="7"): received = queue.Queue() event = {"path": "/health", "httpMethod": "GET", "body": None, "isBase64Encoded": False, "headers": {}, "multiValueHeaders": {}, + "queryStringParameters": {"count": count}, "futureEventField": {"nested": True}, "requestContext": {"requestId": "native-smoke", "identity": {"sourceIp": "127.0.0.1"}}} class RuntimeApi(http.server.BaseHTTPRequestHandler): @@ -41,17 +42,45 @@ def do_POST(self): config = Path(directory) (config / "handler.yml").write_text("""enabled: true handlers: + - com.networknt.aws.lambda.handler.middleware.specification.OpenApiMiddleware@spec + - com.networknt.aws.lambda.handler.middleware.validator.ValidatorMiddleware@validator - com.networknt.aws.lambda.handler.health.HealthCheckHandler@health chains: {} paths: - path: /health method: get - exec: [health] + exec: [spec, validator, health] defaultHandlers: [health] """) (config / "health.yml").write_text("enabled: true\ndownstreamEnabled: false\n") (config / "lambda-app.yml").write_text("lambdaAppId: native-smoke\nencodeBase64Request: false\nencodeBase64Response: false\n") (config / "values.yml").write_text("{}\n") + (config / "openapi-inject.yml").write_text("{}\n") + (config / "openapi-handler.yml").write_text("enabled: true\nmultipleSpec: false\n") + (config / "openapi-validator.yml").write_text("enabled: true\nskipPathPrefixes: []\n") + (config / "openapi.yaml").write_text("""openapi: 3.0.3 +info: + title: Native validation smoke + version: '1' +servers: + - url: / +paths: + /health: + get: + parameters: + - name: count + in: query + required: true + schema: + $ref: '#/components/schemas/Count' + responses: + '200': + description: OK +components: + schemas: + Count: + type: integer +""") server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), RuntimeApi) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() @@ -91,9 +120,13 @@ def main(): assert response["statusCode"] == 200, response assert response["body"] == "OK", response assert response["isBase64Encoded"] is False, response + invalid = invoke(binary, "com.networknt.aws.lambda.app.LambdaApp", "response", count="invalid") + assert invalid["statusCode"] == 400, invalid + assert "ERR11004" in invalid["body"], invalid error = invoke(binary, "native.smoke.MissingHandler", "error") assert "native.smoke.MissingHandler" in error["message"], error print("Native Runtime smoke passed: API Gateway binding, health response and builder error serialization") + print("Native Runtime validation passed: unknown event fields and referenced query-schema rejection") if __name__ == "__main__": From d504eed913e92d0f44be49c26846e1202a2761d2 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Tue, 27 Oct 2026 04:08:12 -0400 Subject: [PATCH 2/7] Provide explicit service configuration for the native smoke fixture --- tests/native_runtime_smoke.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/native_runtime_smoke.py b/tests/native_runtime_smoke.py index f6bbd5e..eeff621 100644 --- a/tests/native_runtime_smoke.py +++ b/tests/native_runtime_smoke.py @@ -55,6 +55,7 @@ def do_POST(self): (config / "health.yml").write_text("enabled: true\ndownstreamEnabled: false\n") (config / "lambda-app.yml").write_text("lambdaAppId: native-smoke\nencodeBase64Request: false\nencodeBase64Response: false\n") (config / "values.yml").write_text("{}\n") + (config / "service.yml").write_text("singletons: []\n") (config / "openapi-inject.yml").write_text("{}\n") (config / "openapi-handler.yml").write_text("enabled: true\nmultipleSpec: false\n") (config / "openapi-validator.yml").write_text("enabled: true\nskipPathPrefixes: []\n") From 3f28b4f4c9ded9c42026538379320f4bb836c696 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Tue, 27 Oct 2026 04:10:25 -0400 Subject: [PATCH 3/7] Make validation error configuration explicit in the runtime smoke --- tests/native_runtime_smoke.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/native_runtime_smoke.py b/tests/native_runtime_smoke.py index eeff621..2255cfd 100644 --- a/tests/native_runtime_smoke.py +++ b/tests/native_runtime_smoke.py @@ -56,6 +56,13 @@ def do_POST(self): (config / "lambda-app.yml").write_text("lambdaAppId: native-smoke\nencodeBase64Request: false\nencodeBase64Response: false\n") (config / "values.yml").write_text("{}\n") (config / "service.yml").write_text("singletons: []\n") + # Native fixtures supply external config explicitly rather than relying on JVM resources. + (config / "status.yml").write_text("""ERR11004: + statusCode: 400 + code: ERR11004 + message: VALIDATOR_SCHEMA + description: Schema Validation Error - %s +""") (config / "openapi-inject.yml").write_text("{}\n") (config / "openapi-handler.yml").write_text("enabled: true\nmultipleSpec: false\n") (config / "openapi-validator.yml").write_text("enabled: true\nskipPathPrefixes: []\n") From 61c6bdbbe7454bf3c9edeee62293847660a54a7d Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Tue, 27 Oct 2026 04:22:57 -0400 Subject: [PATCH 4/7] Register JSON overlay sideband accessors for native OpenAPI validation --- reflect.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/reflect.json b/reflect.json index 2cd12b4..0cce8b5 100644 --- a/reflect.json +++ b/reflect.json @@ -1,4 +1,12 @@ [ + { + "name": "com.networknt.jsonoverlay.MapOverlay$1", + "methods": [{ "name": "getOverlay", "parameterTypes": [] }] + }, + { + "name": "com.networknt.jsonoverlay.ListOverlay$1", + "methods": [{ "name": "getOverlay", "parameterTypes": [] }] + }, { "name": "com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent", "methods": [ From 5a8725c270ee265de73a50be9dcaf2f454493e6f Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Tue, 27 Oct 2026 08:20:56 -0400 Subject: [PATCH 5/7] Register OpenAPI path-cache factories for native execution --- reflect.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/reflect.json b/reflect.json index 0cce8b5..3c85cf3 100644 --- a/reflect.json +++ b/reflect.json @@ -1,4 +1,12 @@ [ + { + "name": "com.github.benmanes.caffeine.cache.SSMS", + "fields": [{ "name": "FACTORY" }] + }, + { + "name": "com.github.benmanes.caffeine.cache.PSMS", + "methods": [{ "name": "", "parameterTypes": [] }] + }, { "name": "com.networknt.jsonoverlay.MapOverlay$1", "methods": [{ "name": "getOverlay", "parameterTypes": [] }] From 0768962d308625768fe5e043213c5d76c9dc2b39 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Tue, 27 Oct 2026 09:21:32 -0400 Subject: [PATCH 6/7] Pin the merged AWS runtime mapper and schema isolation fixes --- ci/jackson3-source-refs.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/jackson3-source-refs.json b/ci/jackson3-source-refs.json index 4607f67..d280a48 100644 --- a/ci/jackson3-source-refs.json +++ b/ci/jackson3-source-refs.json @@ -2,7 +2,7 @@ "http-client": "5ecee29dbb08d813512f5379c2e1b902d553c5d4", "json-overlay": "e309fd63f95317067c9b13a4bed7ac16727bfea5", "light-4j": "97b2359b92ed3e1d05cac04f2da8760aa954e49d", - "light-aws-lambda": "9d59c6a74112a15f702cfd76ceb401680ad05518", + "light-aws-lambda": "5913c7c25b01b6d384fd9c39014dc91d63dfc35e", "light-rest-4j": "a31955945036d9108b7e9bb72167415cc9cfc287", "openapi-parser": "9ae849056e49f7c796fe28bfe6093fbe4847cc5c", "yaml-rule": "c237cc3a25720f43e57bf571e25d5fa06687aa3b" From 13585d518fc61b81793972ee903f7edffab8b909 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Tue, 27 Oct 2026 09:30:53 -0400 Subject: [PATCH 7/7] Expand native validation smoke and use the packaged status catalog --- README.md | 10 ++- .../Jackson3NativeCompatibilityTest.java | 4 +- tests/native_runtime_smoke.py | 80 ++++++++++++++----- 3 files changed, 71 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 114f223..7d4237e 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ python3 tests/test_native_image_script.py ``` These tests stub Maven and Docker to check packaging and failure handling. -They are not run by Maven or the current CI workflow. +They are not run by Maven. The source-build CI workflow runs them separately from the real native smoke. ### Test with Main @@ -127,7 +127,7 @@ software.amazon.awssdk.services.lambda.model.ResourceNotFoundException: Function This source line uses Jackson 3.2.3 and json-schema-validator 3.0.8. Build the Jackson 3 light-4j, http-client, REST and AWS Lambda prerequisites first; older snapshot binaries are not compatible. Jackson annotations retain their com.fasterxml.jackson.annotation package. -The runtime mapper uses Jackson2Defaults with its existing permissive unknown-property override. Mapper instances are immutable. Java time and Optional support are built in. Classic SnakeYAML compatibility pins are retained until the stack-wide cleanup. +The runtime uses the event-only `LambdaRuntimeMappers.mapper()` API from custom-runtime, built from `JacksonDefaults` with its permissive unknown-property override. Mapper instances are immutable. Java time and Optional support are built in. Classic SnakeYAML compatibility pins are retained until the stack-wide cleanup. Local qualification includes the full JVM suite, sanitized event/validation parity against the Jackson 2 baseline, and an offline GraalVM 25 native-image build. A native-image build does not establish deployed AWS behavior or complete reflection coverage for every middleware/configuration. The separate conformance worktree is deferred. @@ -138,7 +138,11 @@ MAVEN_REPO_LOCAL=/path/to/isolated/m2 bash native-image-linux.sh python3 tests/native_runtime_smoke.py target/custom-runtime/lambda-native ``` -The smoke check binds an API Gateway event, runs the local health handler, and verifies success and builder-based error serialization. It uses no AWS endpoint or credentials. `-H:+AddAllCharsets` is required by SnakeYAML Engine's UTF-32 input detection. The check does not qualify every middleware chain or deployed AWS behavior. +The smoke check binds API Gateway events with an unknown future field and runs specification, validator and health handlers. It checks valid referenced query/header/body schemas, invalid query/header/body values, an unknown path, a method mismatch, and builder-based error serialization. The OpenAPI document and handler/validator settings are controlled fixtures. The status catalog and injection settings are copied from the packaged application's `config/status.yml` and `config/config.yml`; their contents are not duplicated in the fixture. By default the script requires exactly one shaded `target/lambda-native-*.jar` (excluding sources and Javadoc JARs); use `--application-jar /path/to/runtime.jar` to select one explicitly. + +It uses no AWS endpoint or credentials. `-H:+AddAllCharsets` is required by SnakeYAML Engine's UTF-32 input detection. The check does not qualify every middleware chain, all default-resource packaging, or deployed AWS behavior. + +The native reflection file currently includes version-specific JSON overlay sideband accessors (`MapOverlay$1.getOverlay()` and `ListOverlay$1.getOverlay()`) and the OpenAPI path cache's Caffeine factories (`SSMS.FACTORY` and `PSMS()`), covered by this smoke. These remain application registrations until their owning libraries ship equivalent native metadata. Migrate native consumers together and remove these entries only after updated source pins and native smoke results establish equivalent coverage; moving them is separate from this mapper change. ### Reproducible Jackson 3 source build diff --git a/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java b/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java index 66baa3e..c7ff9f4 100644 --- a/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java +++ b/src/test/java/com/networknt/aws/lambda/Jackson3NativeCompatibilityTest.java @@ -5,8 +5,8 @@ import org.junit.jupiter.api.Test; import tools.jackson.databind.ObjectMapper; - -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; class Jackson3NativeCompatibilityTest { diff --git a/tests/native_runtime_smoke.py b/tests/native_runtime_smoke.py index 2255cfd..95d387b 100644 --- a/tests/native_runtime_smoke.py +++ b/tests/native_runtime_smoke.py @@ -9,14 +9,11 @@ import subprocess import tempfile import threading +import zipfile -def invoke(binary, handler, expected_endpoint, count="7"): +def invoke(binary, status_catalog, handler, expected_endpoint, event, *, config_catalog): received = queue.Queue() - event = {"path": "/health", "httpMethod": "GET", "body": None, - "isBase64Encoded": False, "headers": {}, "multiValueHeaders": {}, - "queryStringParameters": {"count": count}, "futureEventField": {"nested": True}, - "requestContext": {"requestId": "native-smoke", "identity": {"sourceIp": "127.0.0.1"}}} class RuntimeApi(http.server.BaseHTTPRequestHandler): def log_message(self, *args): @@ -40,6 +37,7 @@ def do_POST(self): with tempfile.TemporaryDirectory(prefix="native-runtime-smoke-") as directory: config = Path(directory) + (config / "config.yml").write_bytes(config_catalog) (config / "handler.yml").write_text("""enabled: true handlers: - com.networknt.aws.lambda.handler.middleware.specification.OpenApiMiddleware@spec @@ -50,19 +48,14 @@ def do_POST(self): - path: /health method: get exec: [spec, validator, health] -defaultHandlers: [health] +defaultHandlers: [spec, validator, health] """) (config / "health.yml").write_text("enabled: true\ndownstreamEnabled: false\n") (config / "lambda-app.yml").write_text("lambdaAppId: native-smoke\nencodeBase64Request: false\nencodeBase64Response: false\n") (config / "values.yml").write_text("{}\n") (config / "service.yml").write_text("singletons: []\n") - # Native fixtures supply external config explicitly rather than relying on JVM resources. - (config / "status.yml").write_text("""ERR11004: - statusCode: 400 - code: ERR11004 - message: VALIDATOR_SCHEMA - description: Schema Validation Error - %s -""") + (config / "status.yml").write_bytes(status_catalog) + # Controlled fixture settings exercise validation; this is not default-config qualification. (config / "openapi-inject.yml").write_text("{}\n") (config / "openapi-handler.yml").write_text("enabled: true\nmultipleSpec: false\n") (config / "openapi-validator.yml").write_text("enabled: true\nskipPathPrefixes: []\n") @@ -81,6 +74,22 @@ def do_POST(self): required: true schema: $ref: '#/components/schemas/Count' + - name: x-count + in: header + required: true + schema: + $ref: '#/components/schemas/Count' + responses: + '200': + description: OK + /body: + post: + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Payload' responses: '200': description: OK @@ -88,6 +97,12 @@ def do_POST(self): schemas: Count: type: integer + Payload: + type: object + required: [value] + properties: + value: + type: integer """) server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), RuntimeApi) thread = threading.Thread(target=server.serve_forever, daemon=True) @@ -122,19 +137,48 @@ def do_POST(self): def main(): parser = argparse.ArgumentParser() parser.add_argument("binary", type=Path) + parser.add_argument("--application-jar", type=Path, + help="Shaded runtime JAR providing framework config and status catalog (default: target/lambda-native-*.jar)") args = parser.parse_args() binary = args.binary.resolve(strict=True) - response = invoke(binary, "com.networknt.aws.lambda.app.LambdaApp", "response") + jars = [args.application_jar] if args.application_jar else [ + jar for jar in (Path(__file__).resolve().parents[1] / "target").glob("lambda-native-*.jar") + if not jar.name.endswith(("-sources.jar", "-javadoc.jar"))] + if len(jars) != 1: + parser.error("Expected one shaded runtime JAR; specify --application-jar explicitly") + with zipfile.ZipFile(jars[0]) as application_jar: + status_catalog = application_jar.read("config/status.yml") + config_catalog = application_jar.read("config/config.yml") + event = {"path": "/health", "httpMethod": "GET", "body": None, + "isBase64Encoded": False, "headers": {"x-count": "7"}, "multiValueHeaders": {}, + "queryStringParameters": {"count": "7"}, "futureEventField": {"nested": True}, + "requestContext": {"requestId": "native-smoke", "identity": {"sourceIp": "127.0.0.1"}}} + handler = "com.networknt.aws.lambda.app.LambdaApp" + response = invoke(binary, status_catalog, handler, "response", event, config_catalog=config_catalog) assert response["statusCode"] == 200, response assert response["body"] == "OK", response assert response["isBase64Encoded"] is False, response - invalid = invoke(binary, "com.networknt.aws.lambda.app.LambdaApp", "response", count="invalid") - assert invalid["statusCode"] == 400, invalid - assert "ERR11004" in invalid["body"], invalid - error = invoke(binary, "native.smoke.MissingHandler", "error") + body_event = event | {"path": "/body", "httpMethod": "POST", "body": '{"value":7}', + "headers": {"content-type": "application/json"}, "queryStringParameters": {}} + body_response = invoke(binary, status_catalog, handler, "response", body_event, config_catalog=config_catalog) + assert body_response["statusCode"] == 200, body_response + assert body_response["body"] == "OK", body_response + cases = [ + ("query", event | {"queryStringParameters": {"count": "invalid"}}, 400, "ERR11004"), + ("header", event | {"headers": {"x-count": "invalid"}}, 400, "ERR11004"), + ("body", body_event | {"body": '{"value":"invalid"}'}, 400, "ERR11004"), + ("unknown path", event | {"path": "/missing"}, 404, "ERR10007"), + ("method mismatch", event | {"httpMethod": "DELETE"}, 405, "ERR10008"), + ] + for name, invalid_event, status, code in cases: + invalid = invoke(binary, status_catalog, handler, "response", invalid_event, config_catalog=config_catalog) + assert invalid["statusCode"] == status, (name, invalid) + assert json.loads(invalid["body"])["code"] == code, (name, invalid) + error = invoke(binary, status_catalog, "native.smoke.MissingHandler", "error", event, config_catalog=config_catalog) assert "native.smoke.MissingHandler" in error["message"], error print("Native Runtime smoke passed: API Gateway binding, health response and builder error serialization") print("Native Runtime validation passed: unknown event fields and referenced query-schema rejection") + print("Native Runtime validation passed: body and header schemas, unknown path and method mismatch") if __name__ == "__main__":