diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/Scope.java b/annot/src/main/java/com/predic8/membrane/annot/generator/Scope.java index d66ec77a73..9bad700504 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/Scope.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/Scope.java @@ -1,3 +1,17 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + package com.predic8.membrane.annot.generator; public enum Scope { diff --git a/annot/src/main/java/com/predic8/membrane/annot/util/ReflectionUtil.java b/annot/src/main/java/com/predic8/membrane/annot/util/ReflectionUtil.java index c55848d6aa..c350c248f3 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/util/ReflectionUtil.java +++ b/annot/src/main/java/com/predic8/membrane/annot/util/ReflectionUtil.java @@ -1,3 +1,17 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + package com.predic8.membrane.annot.util; public class ReflectionUtil { diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/ReflectionUtilTest.java b/annot/src/test/java/com/predic8/membrane/annot/util/ReflectionUtilTest.java index a62f428860..c72ccaa4e1 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/util/ReflectionUtilTest.java +++ b/annot/src/test/java/com/predic8/membrane/annot/util/ReflectionUtilTest.java @@ -1,3 +1,17 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + package com.predic8.membrane.annot.util; import org.junit.jupiter.api.*; diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java index d5f9ea4d28..198210a690 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/ValidatorInterceptor.java @@ -66,6 +66,8 @@ public class ValidatorInterceptor extends AbstractInterceptor implements Applica private ResolverMap resourceResolver; private ApplicationContext applicationContext; + private SchemaMappings schemaMappings; + private SOAPProxy soapProxy; public ValidatorInterceptor() { @@ -94,15 +96,23 @@ public void init() { private MessageValidator getMessageValidator() throws Exception { if (wsdl != null) { + if (schemaMappings != null) + logIgnoringRefSchemas(); return new WSDLValidator(resourceResolver, combine(getBaseLocation(), wsdl), serviceName, createFailureHandler(), skipFaults); } if (schema != null) { + if (schemaMappings != null) + logIgnoringRefSchemas(); return new XMLSchemaValidator(resourceResolver, combine(getBaseLocation(), schema), createFailureHandler()); } if (jsonSchema != null) { - return new JSONYAMLSchemaValidator(resourceResolver, combine(getBaseLocation(), jsonSchema), createFailureHandler(), schemaVersion); + return new JSONYAMLSchemaValidator(resourceResolver, combine(getBaseLocation(), jsonSchema), createFailureHandler(), schemaVersion) {{ + if(schemaMappings != null) setSchemaMappings(schemaMappings.getSchemaMap()); + }}; } if (schematron != null) { + if (schemaMappings != null) + logIgnoringRefSchemas(); return new SchematronValidator(combine(getBaseLocation(), schematron), createFailureHandler(), router, applicationContext); } @@ -112,6 +122,10 @@ private MessageValidator getMessageValidator() throws Exception { throw new RuntimeException("Validator is not configured properly. must have an attribute specifying the validator."); } + private static void logIgnoringRefSchemas() { + log.warn("Ignoring 'referenceSchemas': schema references are only supported for JSON/YAML validators"); + } + private @Nullable WSDLValidator getWsdlValidatorFromSOAPProxy() { if(soapProxy == null) return null; wsdl = soapProxy.getWsdl(); @@ -319,6 +333,16 @@ private FailureHandler createFailureHandler() { throw new IllegalArgumentException("Unknown failureHandler type: " + failureHandler); } + @MCChildElement + public void setReferenceSchemas(SchemaMappings schemaMappings) { + this.schemaMappings = schemaMappings; + } + + public SchemaMappings getReferenceSchemas() { + return schemaMappings; + } + + public void setSoapProxy(SOAPProxy soapProxy) { this.soapProxy = soapProxy; } diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java index 1c4ee4218e..ca88c683d9 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/JSONYAMLSchemaValidator.java @@ -20,6 +20,7 @@ import com.networknt.schema.*; import com.networknt.schema.Error; import com.networknt.schema.path.NodePath; +import com.networknt.schema.resource.SchemaLoader; import com.predic8.membrane.core.exchange.*; import com.predic8.membrane.core.interceptor.Interceptor.*; import com.predic8.membrane.core.interceptor.*; @@ -48,7 +49,6 @@ public class JSONYAMLSchemaValidator extends AbstractMessageValidator { private final YAMLFactory factory = YAMLFactory.builder().enable(STRICT_DUPLICATE_DETECTION).build(); private final ObjectMapper yamlObjectMapper = new ObjectMapper(factory); - private final ObjectMapper jsonObjectMapper = new ObjectMapper(); public static final String SCHEMA_VERSION_2020_12 = "2020-12"; @@ -60,10 +60,7 @@ public class JSONYAMLSchemaValidator extends AbstractMessageValidator { private final AtomicLong invalid = new AtomicLong(); private final SpecificationVersion schemaId; - /** - * JsonSchemaFactory instances are thread-safe provided its configuration is not modified. - */ - SchemaRegistry jsonSchemaFactory; + private Map schemaMappings = new HashMap<>(); /** * JsonSchema instances are thread-safe provided its configuration is not modified. @@ -76,7 +73,7 @@ public JSONYAMLSchemaValidator(Resolver resolver, String jsonSchema, FailureHand this.resolver = resolver; this.jsonSchema = jsonSchema; this.failureHandler = failureHandler; - this.schemaId = JSONSchemaVersionParser.parse( schemaVersion); + this.schemaId = JSONSchemaVersionParser.parse(schemaVersion); this.inputFormat = inputFormat; } @@ -97,17 +94,25 @@ public String getName() { public void init() { super.init(); - jsonSchemaFactory = SchemaRegistry.withDefaultDialect(schemaId, builder -> - builder.schemaLoader(loaders -> new MembraneSchemaLoader(resolver))); - try (InputStream in = resolver.resolve(jsonSchema)) { - schema = jsonSchemaFactory.getSchema((jsonSchema.endsWith(".yaml") || jsonSchema.endsWith(".yml") ? yamlObjectMapper: jsonObjectMapper).readTree(in)); + schema = createSchemaRegistry().getSchema(SchemaLocation.of(jsonSchema), in, getSchemaFormat()); schema.initializeValidators(); } catch (IOException e) { throw new RuntimeException("Cannot read JSON Schema from: " + jsonSchema, e); } } + private @NotNull InputFormat getSchemaFormat() { + return (jsonSchema.toLowerCase().endsWith(".yaml") || jsonSchema.toLowerCase().endsWith(".yml")) ? YAML : JSON; + } + + private SchemaRegistry createSchemaRegistry() { + return SchemaRegistry.withDefaultDialect(schemaId, b -> b.schemaLoader(SchemaLoader.builder() + .schemaIdResolvers(r -> r.mappings(schemaMappings)) + .resourceLoaders(rl -> rl.values(list -> list.addFirst(new MembraneSchemaLoader(resolver)))) + .build())); + } + public Outcome validateMessage(Exchange exc, Flow flow) throws Exception { return validateMessage(exc, flow, UTF_8); } @@ -115,8 +120,8 @@ public Outcome validateMessage(Exchange exc, Flow flow) throws Exception { public Outcome validateMessage(Exchange exc, Flow flow, Charset ignored) throws Exception { List assertions = inputFormat == YAML ? - handleMultipleYAMLDocuments(exc, flow) : - schema.validate(exc.getMessage(flow).getBodyAsStringDecoded(), inputFormat); + handleMultipleYAMLDocuments(exc, flow) : + schema.validate(exc.getMessage(flow).getBodyAsStringDecoded(), inputFormat); if (assertions.isEmpty()) { valid.incrementAndGet(); @@ -204,4 +209,8 @@ public long getInvalid() { public String getErrorTitle() { return "JSON validation failed"; } + + public void setSchemaMappings(Map schemaMappings) { + this.schemaMappings = schemaMappings; + } } \ No newline at end of file diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/SchemaMappings.java b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/SchemaMappings.java new file mode 100644 index 0000000000..368c2625f6 --- /dev/null +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/schemavalidation/json/SchemaMappings.java @@ -0,0 +1,72 @@ +/* Copyright 2025 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.core.interceptor.schemavalidation.json; + +import com.predic8.membrane.annot.MCAttribute; +import com.predic8.membrane.annot.MCChildElement; +import com.predic8.membrane.annot.MCElement; +import com.predic8.membrane.annot.Required; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@MCElement(name = "schemaMappings") +public class SchemaMappings { + + private List schemas = new ArrayList<>(); + + public Map getSchemaMap() { + Map referenceSchemas = new HashMap<>(); + schemas.forEach(schema -> referenceSchemas.put(schema.getId(), schema.getLocation())); + return referenceSchemas; + } + + @Required + @MCChildElement + public void setSchemas(List schemas) { + this.schemas = schemas; + } + + public List getSchemas() { + return schemas; + } + + @MCElement(name = "schema") + public static class Schema { + private String id; + + private String location; + + @MCAttribute + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + @MCAttribute + public void setLocation(String location) { + this.location = location; + } + + public String getLocation() { + return location; + } + } +} diff --git a/distribution/examples/validation/json-schema/schema-mappings/README.md b/distribution/examples/validation/json-schema/schema-mappings/README.md new file mode 100644 index 0000000000..9b3e893a2c --- /dev/null +++ b/distribution/examples/validation/json-schema/schema-mappings/README.md @@ -0,0 +1,51 @@ +# Validation - JSON Schema with Schema Mappings + +This sample explains how to set up and use the `validator` plugin with JSON Schemas that reference external schemas +by `$ref` URN/ID, and how to map those references to local files via `schemaMappings`. + +## Running the Example + +1. Go to the directory: + `/examples/validation/json-schema/schema-mappings` + +2. Start `membrane.cmd` or `membrane.sh`. + +3. Look at `schemas/schema2000.json` and note the `$ref` to `urn:app:base_parameter_def`. Then open `schemas/base-param.json` and compare the schema to `good2000.json` and `bad2000.json`. + +4. Run `curl -H "Content-Type: application/json" -d @good2000.json http://localhost:2000/` on the console. Observe that you get a successful response. + +5. Run `curl -H "Content-Type: application/json" -d @bad2000.json http://localhost:2000/`. Observe that you get a validation error response. + +Keeping the router running, you can try a more complex setup with multiple referenced schemas. + +1. Have a look at `schemas/schema2001.json` and note the `$ref`s to `urn:app:base_parameter_def` and `urn:app:meta_def`. Then open `schemas/base-param.json` and `schemas/meta.json` and compare the schemas to `good2001.json` and `bad2001.json`. + +2. Run `curl -H "Content-Type: application/json" -d @good2001.json http://localhost:2001/`. Observe that you get a successful response. + +3. Run `curl -H "Content-Type: application/json" -d @bad2001.json http://localhost:2001/`. Observe that you get a validation error response. + +## How it is done + +In `proxies.xml`, each API configures a ``. +The root schemas contain `$ref` references to URN/IDs, for example: + +- `urn:app:base_parameter_def#/$defs/BaseParameter` +- `urn:app:meta_def#/$defs/Meta` + +To let Membrane resolve these URNs, you map them in the validator using: + +```xml + + + + +```` + +Only if validation succeeds, the request is forwarded to the backend (port 2002). + +--- + +See: + +* [JSON Schema](https://json-schema.org/) documentation +* [validator](https://www.membrane-api.io/docs/current/validator.html) reference \ No newline at end of file diff --git a/distribution/examples/validation/json-schema/schema-mappings/bad2000.json b/distribution/examples/validation/json-schema/schema-mappings/bad2000.json new file mode 100644 index 0000000000..80cd797310 --- /dev/null +++ b/distribution/examples/validation/json-schema/schema-mappings/bad2000.json @@ -0,0 +1,7 @@ +{ + "param": { + "name": "limit" + }, + "timestamp": "not-a-date", + "extra": true +} diff --git a/distribution/examples/validation/json-schema/schema-mappings/bad2001.json b/distribution/examples/validation/json-schema/schema-mappings/bad2001.json new file mode 100644 index 0000000000..be22cf9ee3 --- /dev/null +++ b/distribution/examples/validation/json-schema/schema-mappings/bad2001.json @@ -0,0 +1,12 @@ +{ + "params": [ + { + "name": "mode", + "value": "fast", + "unexpected": "foo" + } + ], + "meta": { + "source": "" + } +} diff --git a/distribution/examples/validation/json-schema/schema-mappings/good2000.json b/distribution/examples/validation/json-schema/schema-mappings/good2000.json new file mode 100644 index 0000000000..c9214c7149 --- /dev/null +++ b/distribution/examples/validation/json-schema/schema-mappings/good2000.json @@ -0,0 +1,7 @@ +{ + "param": { + "name": "limit", + "value": 100 + }, + "timestamp": "2025-12-19T10:15:30Z" +} diff --git a/distribution/examples/validation/json-schema/schema-mappings/good2001.json b/distribution/examples/validation/json-schema/schema-mappings/good2001.json new file mode 100644 index 0000000000..0c64ff9d6f --- /dev/null +++ b/distribution/examples/validation/json-schema/schema-mappings/good2001.json @@ -0,0 +1,16 @@ +{ + "params": [ + { + "name": "mode", + "value": "fast" + }, + { + "name": "retries", + "value": 3 + } + ], + "meta": { + "source": "curl", + "requestId": "REQ-12345678" + } +} diff --git a/distribution/examples/validation/json-schema/schema-mappings/membrane.cmd b/distribution/examples/validation/json-schema/schema-mappings/membrane.cmd new file mode 100644 index 0000000000..8d2d64e9cf --- /dev/null +++ b/distribution/examples/validation/json-schema/schema-mappings/membrane.cmd @@ -0,0 +1,24 @@ +@echo off +setlocal EnableExtensions + +set "SCRIPT_DIR=%~dp0" +if "%SCRIPT_DIR:~-1%"=="\" set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" + +set "dir=%SCRIPT_DIR%" + +:search_up +if exist "%dir%\LICENSE.txt" if exist "%dir%\scripts\run-membrane.cmd" goto found +for %%A in ("%dir%\..") do set "next=%%~fA" +if /I "%next%"=="%dir%" goto notfound +set "dir=%next%" +goto search_up + +:found +set "MEMBRANE_HOME=%dir%" +set "MEMBRANE_CALLER_DIR=%SCRIPT_DIR%" +call "%MEMBRANE_HOME%\scripts\run-membrane.cmd" %* +exit /b %ERRORLEVEL% + +:notfound +>&2 echo Could not locate Membrane root. Ensure directory structure is correct. +exit /b 1 diff --git a/distribution/examples/validation/json-schema/schema-mappings/membrane.sh b/distribution/examples/validation/json-schema/schema-mappings/membrane.sh new file mode 100755 index 0000000000..195dae51ec --- /dev/null +++ b/distribution/examples/validation/json-schema/schema-mappings/membrane.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Default: ./proxies.xml (next to this script); fallback -> $MEMBRANE_HOME/conf/proxies.xml +# JAVA_OPTS: relative -D paths are auto-resolved against $MEMBRANE_HOME (absolute/URI unchanged). +# Examples: +# export JAVA_OPTS='-Dlog4j.configurationFile=examples/logging/access/log4j2_access.xml' +# export JAVA_OPTS='-Dlog4j.configurationFile=/abs/path/log4j2.xml' + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P) + +dir="$SCRIPT_DIR" +while [ "$dir" != "/" ]; do + if [ -f "$dir/LICENSE.txt" ] && [ -f "$dir/scripts/run-membrane.sh" ]; then + export MEMBRANE_HOME="$dir" + export MEMBRANE_CALLER_DIR="$SCRIPT_DIR" + exec sh "$dir/scripts/run-membrane.sh" "$@" + fi + dir=$(dirname "$dir") +done + +echo "Could not locate Membrane root. Ensure directory structure is correct." >&2 +exit 1 \ No newline at end of file diff --git a/distribution/examples/validation/json-schema/schema-mappings/proxies.xml b/distribution/examples/validation/json-schema/schema-mappings/proxies.xml new file mode 100644 index 0000000000..b0190904d3 --- /dev/null +++ b/distribution/examples/validation/json-schema/schema-mappings/proxies.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Response.ok("<response>good request</response>").build() + + + + + + diff --git a/distribution/examples/validation/json-schema/schema-mappings/schemas/base-param.json b/distribution/examples/validation/json-schema/schema-mappings/schemas/base-param.json new file mode 100644 index 0000000000..0da01fe2c3 --- /dev/null +++ b/distribution/examples/validation/json-schema/schema-mappings/schemas/base-param.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:app:base_parameter_def", + "$defs": { + "BaseParameter": { + "type": "object", + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "value": {} + }, + "additionalProperties": false + } + } +} diff --git a/distribution/examples/validation/json-schema/schema-mappings/schemas/meta.json b/distribution/examples/validation/json-schema/schema-mappings/schemas/meta.json new file mode 100644 index 0000000000..3379d26ec6 --- /dev/null +++ b/distribution/examples/validation/json-schema/schema-mappings/schemas/meta.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:app:meta_def", + "$defs": { + "Meta": { + "type": "object", + "required": [ + "source", + "requestId" + ], + "properties": { + "source": { + "type": "string", + "minLength": 1 + }, + "requestId": { + "type": "string", + "minLength": 8 + } + }, + "additionalProperties": false + } + } +} diff --git a/distribution/examples/validation/json-schema/schema-mappings/schemas/schema2000.json b/distribution/examples/validation/json-schema/schema-mappings/schemas/schema2000.json new file mode 100644 index 0000000000..0293b82841 --- /dev/null +++ b/distribution/examples/validation/json-schema/schema-mappings/schemas/schema2000.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:app:schema2000", + "type": "object", + "required": [ + "param", + "timestamp" + ], + "properties": { + "param": { + "$ref": "urn:app:base_parameter_def#/$defs/BaseParameter" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + }, + "additionalProperties": false +} diff --git a/distribution/examples/validation/json-schema/schema-mappings/schemas/schema2001.json b/distribution/examples/validation/json-schema/schema-mappings/schemas/schema2001.json new file mode 100644 index 0000000000..ef886181f6 --- /dev/null +++ b/distribution/examples/validation/json-schema/schema-mappings/schemas/schema2001.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "urn:app:schema2001", + "type": "object", + "required": [ + "params", + "meta" + ], + "properties": { + "params": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "urn:app:base_parameter_def#/$defs/BaseParameter" + } + }, + "meta": { + "$ref": "urn:app:meta_def#/$defs/Meta" + } + }, + "additionalProperties": false +} diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaMappingsExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaMappingsExampleTest.java new file mode 100644 index 0000000000..3804aef014 --- /dev/null +++ b/distribution/src/test/java/com/predic8/membrane/examples/withoutinternet/validation/JSONSchemaMappingsExampleTest.java @@ -0,0 +1,132 @@ +/* Copyright 2012 predic8 GmbH, www.predic8.com + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. */ + +package com.predic8.membrane.examples.withoutinternet.validation; + +import com.predic8.membrane.examples.util.*; +import org.junit.jupiter.api.*; + +import static com.predic8.membrane.core.http.MimeType.*; +import static io.restassured.RestAssured.*; +import static io.restassured.http.ContentType.*; +import static java.io.File.*; +import static org.hamcrest.Matchers.*; + +public class JSONSchemaMappingsExampleTest extends DistributionExtractingTestcase { + + @Override + protected String getExampleDirName() { + return "validation" + separator + "json-schema" + separator + "schema-mappings"; + } + + @Test + void port2000() throws Exception { + try (Process2 ignored = startServiceProxyScript()) { + + // @formatter:off + // Test good JSON + given() + .contentType(JSON) + .body(readFileFromBaseDir("good2000.json")) + .when() + .post("http://localhost:2000") + .then() + .statusCode(200); + + // Test bad JSON + given() + .contentType(JSON) + .body(readFileFromBaseDir("bad2000.json")) + .when() + .post("http://localhost:2000") + .then() + .statusCode(400) + .contentType(APPLICATION_PROBLEM_JSON) + .body("title", equalTo("JSON validation failed")) + .body("type", equalTo("https://membrane-api.io/problems/user/validation")) + .body("status", equalTo(400)) + .body("flow", equalTo("REQUEST")) + + // error 1: required value in param + .body("errors.find { it.pointer == '/properties/param/$ref/required' }.key", equalTo("required")) + .body("errors.find { it.pointer == '/properties/param/$ref/required' }.keyword", equalTo("required")) + .body("errors.find { it.pointer == '/properties/param/$ref/required' }.message", equalTo("required property 'value' not found")) + .body("errors.find { it.pointer == '/properties/param/$ref/required' }.details.property", equalTo("value")) + + // error 2: extra additional property + .body("errors.find { it.pointer == '/additionalProperties' }.key", equalTo("additionalProperties")) + .body("errors.find { it.pointer == '/additionalProperties' }.keyword", equalTo("additionalProperties")) + .body("errors.find { it.pointer == '/additionalProperties' }.message", equalTo("property 'extra' is not defined in the schema and the schema does not allow additional properties")) + .body("errors.find { it.pointer == '/additionalProperties' }.details.property", equalTo("extra")) + + // meta fields + .body("see", equalTo("https://membrane-api.io/problems/user/validation/json-schema-validator")) + .body("attention", containsString("development mode")); + // @formatter:on + } + } + + @Test + void port2001() throws Exception { + try (Process2 ignored = startServiceProxyScript()) { + + // @formatter:off + // Test good JSON + given() + .contentType(JSON) + .body(readFileFromBaseDir("good2001.json")) + .when() + .post("http://localhost:2001") + .then() + .statusCode(200); + + // Test bad JSON + given() + .contentType(JSON) + .body(readFileFromBaseDir("bad2001.json")) + .when() + .post("http://localhost:2001") + .then() + .statusCode(400) + .contentType(APPLICATION_PROBLEM_JSON) + .body("title", equalTo("JSON validation failed")) + .body("type", equalTo("https://membrane-api.io/problems/user/validation")) + .body("status", equalTo(400)) + .body("flow", equalTo("REQUEST")) + + // error 1: unexpected additional property in params[0] + .body("errors.find { it.pointer == '/properties/params/items/$ref/additionalProperties' }.key", equalTo("additionalProperties")) + .body("errors.find { it.pointer == '/properties/params/items/$ref/additionalProperties' }.keyword", equalTo("additionalProperties")) + .body("errors.find { it.pointer == '/properties/params/items/$ref/additionalProperties' }.message", equalTo("property 'unexpected' is not defined in the schema and the schema does not allow additional properties")) + .body("errors.find { it.pointer == '/properties/params/items/$ref/additionalProperties' }.details.property", equalTo("unexpected")) + + // error 2: meta.source minLength + .body("errors.find { it.pointer == '/properties/meta/$ref/properties/source/minLength' }.key", equalTo("minLength")) + .body("errors.find { it.pointer == '/properties/meta/$ref/properties/source/minLength' }.keyword", equalTo("minLength")) + .body("errors.find { it.pointer == '/properties/meta/$ref/properties/source/minLength' }.message", equalTo("must be at least 1 characters long")) + + // error 3: meta.requestId required + .body("errors.find { it.pointer == '/properties/meta/$ref/required' }.key", equalTo("required")) + .body("errors.find { it.pointer == '/properties/meta/$ref/required' }.keyword", equalTo("required")) + .body("errors.find { it.pointer == '/properties/meta/$ref/required' }.message", equalTo("required property 'requestId' not found")) + .body("errors.find { it.pointer == '/properties/meta/$ref/required' }.details.property", equalTo("requestId")) + + // meta fields + .body("see", equalTo("https://membrane-api.io/problems/user/validation/json-schema-validator")) + .body("attention", containsString("development mode")); + // @formatter:on + } + } + +}