This is an automated email from the ASF dual-hosted git repository.
pefernan pushed a commit to branch main
in repository
https://gitbox.apache.org/repos/asf/incubator-kie-kogito-runtimes.git
The following commit(s) were added to refs/heads/main by this push:
new 901c5908b2 [incubator-kie-issues-2367] PATCH API ignores explicit JSON
null values for String process variables (#4335)
901c5908b2 is described below
commit 901c5908b2ee7659033ff963bdd6b2cf8992cb5e
Author: Alisha Mohamed Ali <[email protected]>
AuthorDate: Fri Jul 17 18:25:52 2026 +0530
[incubator-kie-issues-2367] PATCH API ignores explicit JSON null values for
String process variables (#4335)
* PATCH API ignores explicit JSON null values for String process variables
* Make present fieds hidden
* Preserve explicit null vs omitted field distinction on PATCH
* fix comments
* fix buid chain
* fix buid chain
* Update MapOutput.java
* fix formating
* Adding tests
* add tests
* fix formatimg error
* Add tests to have both PATCH and PUT calls on a resource,
---------
Co-authored-by: Alisha Mohamed Ali <[email protected]>
---
.../src/main/java/org/kie/kogito/Model.java | 13 +-
.../src/main/java/org/kie/kogito/Models.java | 8 +-
.../org/jbpm/compiler/canonical/ModelMetaData.java | 25 +++-
.../canonical/ProcessToExecModelGenerator.java | 2 +
.../class-templates/ModelNoIDTemplate.java | 21 ++-
.../resources/class-templates/ModelTemplate.java | 35 ++++-
.../RestResourceQuarkusTemplate.java | 4 +-
.../RestResourceSpringTemplate.java | 4 +-
.../integrationtests/quarkus/BasicRestIT.java | 86 +++++++++++
.../kie/kogito/integrationtests/quarkus/OASIT.java | 21 +++
.../src/main/resources/AdHocFragments.bpmn | 2 +
.../integrationtests/springboot/BasicRestTest.java | 159 +++++++++++++++++++++
12 files changed, 370 insertions(+), 10 deletions(-)
diff --git a/api/kogito-api/src/main/java/org/kie/kogito/Model.java
b/api/kogito-api/src/main/java/org/kie/kogito/Model.java
index fbe9acb8de..a4583aa74f 100644
--- a/api/kogito-api/src/main/java/org/kie/kogito/Model.java
+++ b/api/kogito-api/src/main/java/org/kie/kogito/Model.java
@@ -19,8 +19,7 @@
package org.kie.kogito;
import java.util.Map;
-import java.util.Map.Entry;
-import java.util.stream.Collectors;
+import java.util.Set;
/**
* Represents data model type of objects that are usually descriptor of data
holders.
@@ -33,8 +32,16 @@ public interface Model extends MapInput, MapOutput {
}
default Map<String, Object> updatePartially(Map<String, Object> params) {
- params = params.entrySet().stream().filter(e -> e.getValue() !=
null).collect(Collectors.toMap(Entry::getKey, Entry::getValue));
update(params);
return params;
}
+
+ /**
+ * Returns the set of field names that were explicitly set (e.g. during a
PATCH request).
+ * Returns null when tracking is not active — all fields are included in
toMap().
+ * Returns a non-null set when tracking is active — only those fields are
included.
+ */
+ default Set<String> getModifiedFields() {
+ return null;
+ }
}
diff --git a/api/kogito-api/src/main/java/org/kie/kogito/Models.java
b/api/kogito-api/src/main/java/org/kie/kogito/Models.java
index c1e57052d6..c91ca522fe 100644
--- a/api/kogito-api/src/main/java/org/kie/kogito/Models.java
+++ b/api/kogito-api/src/main/java/org/kie/kogito/Models.java
@@ -21,6 +21,7 @@ package org.kie.kogito;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
import java.util.Map;
+import java.util.Set;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -30,12 +31,17 @@ public class Models {
}
@SuppressWarnings("squid:S3011")
- public static Map<String, Object> toMap(Object m) {
+ public static Map<String, Object> toMap(MapOutput m) {
+ Set<String> modifiedFields = (m instanceof Model model) ?
model.getModifiedFields() : null;
Map<String, Object> map = new LinkedHashMap<>();
for (Field field : m.getClass().getDeclaredFields()) {
JsonProperty jsonAnnotation =
field.getAnnotation(JsonProperty.class);
if (jsonAnnotation != null) {
String name = jsonAnnotation.value();
+ // null = not tracking (include all); non-null = include only
modified fields
+ if (modifiedFields != null && !modifiedFields.contains(name)) {
+ continue;
+ }
field.setAccessible(true);
try {
map.put(name, field.get(m));
diff --git
a/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ModelMetaData.java
b/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ModelMetaData.java
index 62f35f59cd..bd3d64d21a 100644
---
a/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ModelMetaData.java
+++
b/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ModelMetaData.java
@@ -190,6 +190,18 @@ public class ModelMetaData {
}
modelClass.setName(modelClassSimpleName);
+ // The modified-fields tracking field/getter must stay out of the
generated OpenAPI
+ if (supportsOpenApiGeneration) {
+ NormalAnnotationExpr hiddenSchema = new NormalAnnotationExpr(new
Name(Schema.class.getCanonicalName()),
+ NodeList.nodeList(new MemberValuePair("hidden", new
BooleanLiteralExpr(true))));
+ modelClass.findFirst(FieldDeclaration.class,
+ fd -> fd.getVariables().stream().anyMatch(v ->
v.getNameAsString().equals("__modifiedFields")))
+ .ifPresent(fd -> fd.addAnnotation(hiddenSchema.clone()));
+ modelClass.findFirst(MethodDeclaration.class, md ->
md.getNameAsString().equals("getModifiedFields"))
+ .ifPresent(md -> md.addAnnotation(hiddenSchema.clone()));
+ compilationUnit.addImport(Schema.class.getCanonicalName());
+ }
+
// setup of the toMap method body
BlockStmt toMapBody = new BlockStmt();
ClassOrInterfaceType toMap = new ClassOrInterfaceType(null, new
SimpleName(Map.class.getSimpleName()),
@@ -225,7 +237,14 @@ public class ModelMetaData {
applyOpenApiSchemaAnnotation(fd);
fd.createGetter();
- fd.createSetter();
+ MethodDeclaration setter = fd.createSetter();
+ if (isInputModel()) {
+ // Only Input setters run from request deserialization -
Model/Output setters
+ // are also called by generated copy loops, which would
wrongly count as "set".
+ setter.getBody().ifPresent(setterBody ->
setterBody.addStatement(
+ new MethodCallExpr(null, "markModified",
+ NodeList.nodeList(new
StringLiteralExpr(varName)))));
+ }
}
@@ -265,6 +284,10 @@ public class ModelMetaData {
.addModifier(Modifier.Keyword.PRIVATE);
}
+ private boolean isInputModel() {
+ return "/class-templates/ModelNoIDTemplate.java".equals(templateName);
+ }
+
public String getModelClassSimpleName() {
return modelClassSimpleName;
}
diff --git
a/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ProcessToExecModelGenerator.java
b/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ProcessToExecModelGenerator.java
index b78ed1741f..1692b928f5 100644
---
a/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ProcessToExecModelGenerator.java
+++
b/jbpm/jbpm-flow-builder/src/main/java/org/jbpm/compiler/canonical/ProcessToExecModelGenerator.java
@@ -222,6 +222,8 @@ public class ProcessToExecModelGenerator {
"get" +
fieldName)));
}
+ body.addStatement(new MethodCallExpr(returnName,
"addModifiedFields",
+ NodeList.nodeList(new MethodCallExpr(null,
"getModifiedFields"))));
body.addStatement(new ReturnStmt(returnName));
method.setBody(body);
}
diff --git
a/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelNoIDTemplate.java
b/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelNoIDTemplate.java
index c5d6fdd049..f66bd652d5 100644
---
a/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelNoIDTemplate.java
+++
b/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelNoIDTemplate.java
@@ -24,11 +24,30 @@ import org.kie.kogito.MapOutput;
import java.util.Map;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
import org.kie.kogito.MappableToModel;
import org.kie.kogito.Model;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
public class XXXModel implements Model, MapInput, MapInputId, MapOutput,
MappableToModel<$modelClass$> {
-}
\ No newline at end of file
+ // Ephemeral REST request-body carrier - always tracks modifications from
construction.
+ @JsonIgnore
+ private transient final Set<String> __modifiedFields = new HashSet<>();
+
+ @JsonIgnore
+ @Override
+ public Set<String> getModifiedFields() {
+ return __modifiedFields;
+ }
+
+ // Marks a field as explicitly set, so PATCH can tell a null apart from an
omitted field.
+ void markModified(String field) {
+ __modifiedFields.add(field);
+ }
+
+}
diff --git
a/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelTemplate.java
b/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelTemplate.java
index b33106e9e8..b74cb6e7b3 100644
---
a/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelTemplate.java
+++
b/jbpm/jbpm-flow-builder/src/main/resources/class-templates/ModelTemplate.java
@@ -24,20 +24,51 @@ import org.kie.kogito.MapOutput;
import java.util.Map;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
import org.kie.kogito.MappableToModel;
import org.kie.kogito.Model;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
public class XXXModel implements org.kie.kogito.Model, MapInput, MapInputId,
MapOutput, MappableToModel<$modelClass$> {
private String id;
+ @JsonIgnore
+ private transient Set<String> __modifiedFields;
+
public void setId(String id) {
this.id = id;
}
-
+
public String getId() {
return this.id;
}
-}
\ No newline at end of file
+ @JsonIgnore
+ @Override
+ public Set<String> getModifiedFields() {
+ return __modifiedFields;
+ }
+
+ // Codegen-internal: carries the modified-fields set from a PATCH Input
onto
+ // its toModel() target. Left null otherwise, so toMap() includes every
field.
+ void addModifiedFields(Set<String> fields) {
+ if (fields == null) {
+ return;
+ }
+ if (__modifiedFields == null) {
+ __modifiedFields = new HashSet<>();
+ }
+ __modifiedFields.addAll(fields);
+ }
+
+ // Called by the generated PUT handler before a full-replace update, so
every
+ // field counts as significant - unlike PATCH, which only touches tracked
fields.
+ void clearModifiedFields() {
+ __modifiedFields = null;
+ }
+
+}
diff --git
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java
index 6202d1032e..18d5e778b2 100644
---
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java
+++
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceQuarkusTemplate.java
@@ -160,7 +160,9 @@ public class $Type$Resource {
@Produces(MediaType.APPLICATION_JSON)
@Operation(operationId = "updateProcessInstance_$name$", summary =
"$documentation$", description = "$processInstanceDescription$")
public $Type$Output updateModel_$name$(@PathParam("id") String id,
$Type$Input resource) {
- return processService.update(process, id,
resource.toModel()).orElseThrow(NotFoundException::new);
+ $Type$ model = resource.toModel();
+ model.clearModifiedFields();
+ return processService.update(process, id,
model).orElseThrow(NotFoundException::new);
}
@PATCH
diff --git
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java
index fe227d7538..5328f80d17 100644
---
a/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java
+++
b/kogito-codegen-modules/kogito-codegen-processes/src/main/resources/class-templates/RestResourceSpringTemplate.java
@@ -112,7 +112,9 @@ public class $Type$Resource {
consumes = MediaType.APPLICATION_JSON_VALUE)
@Operation(operationId = "updateProcessInstance_$name$", summary =
"$documentation$", description = "$processInstanceDescription$")
public $Type$Output updateModel_$name$(@PathVariable("id") String id,
@RequestBody(required = false) $Type$Input resource) {
- return processService.update(process, id,
resource.toModel()).orElseThrow(() -> new
ResponseStatusException(HttpStatus.NOT_FOUND));
+ $Type$ model = resource.toModel();
+ model.clearModifiedFields();
+ return processService.update(process, id, model).orElseThrow(() -> new
ResponseStatusException(HttpStatus.NOT_FOUND));
}
@PatchMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE,
diff --git
a/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/BasicRestIT.java
b/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/BasicRestIT.java
index 48fc895af4..39f5268b84 100644
---
a/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/BasicRestIT.java
+++
b/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/BasicRestIT.java
@@ -252,6 +252,92 @@ class BasicRestIT {
assertExpectedUnitOfWorkEvents(2);
}
+ @Test
+ void testPatchWithNullValue() {
+ String id = given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body(getParams())
+ .post("/AdHocFragments")
+ .then()
+ .statusCode(201)
+ .header("Location", not(emptyOrNullString()))
+ .body("id", not(emptyOrNullString()))
+ .body("var1", equalTo("Kermit"))
+ .body("var2", equalTo(34))
+ .extract()
+ .path("id");
+
+ // var1 set to null must be cleared, unlike an omitted field
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body("{\"var1\": null}")
+ .patch("/AdHocFragments/{customId}", id)
+ .then()
+ .statusCode(200)
+ .body("id", equalTo(id))
+ .body("var1", nullValue())
+ .body("var2", is(34));
+
+ assertExpectedUnitOfWorkEvents(2);
+ }
+
+ @Test
+ void testPatchAndPutSequence() {
+ String id = given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body(getParams())
+ .post("/AdHocFragments")
+ .then()
+ .statusCode(201)
+ .header("Location", not(emptyOrNullString()))
+ .body("id", not(emptyOrNullString()))
+ .body("var1", equalTo("Kermit"))
+ .body("var2", equalTo(34))
+ .extract()
+ .path("id");
+
+ // PATCH: var1 explicitly nulled, var2 untouched
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body("{\"var1\": null}")
+ .patch("/AdHocFragments/{customId}", id)
+ .then()
+ .statusCode(200)
+ .body("id", equalTo(id))
+ .body("var1", nullValue())
+ .body("var2", is(34));
+
+ // PUT right after a PATCH on the same resource: var2 omitted -> must
still be nulled out
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body("{\"var1\": \"Gonzo\"}")
+ .put("/AdHocFragments/{customId}", id)
+ .then()
+ .statusCode(200)
+ .body("id", equalTo(id))
+ .body("var1", equalTo("Gonzo"))
+ .body("var2", nullValue());
+
+ // PATCH right after a PUT: empty body must leave both fields untouched
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body("{}")
+ .patch("/AdHocFragments/{customId}", id)
+ .then()
+ .statusCode(200)
+ .body("id", equalTo(id))
+ .body("var1", equalTo("Gonzo"))
+ .body("var2", nullValue());
+
+ assertExpectedUnitOfWorkEvents(4);
+ }
+
@Test
void testDelete() {
Map<String, String> params = new HashMap<>();
diff --git
a/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/OASIT.java
b/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/OASIT.java
index 50575452ae..9db03fe82f 100644
---
a/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/OASIT.java
+++
b/quarkus/integration-tests/integration-tests-quarkus-processes/src/test/java/org/kie/kogito/integrationtests/quarkus/OASIT.java
@@ -27,6 +27,7 @@ import io.quarkus.test.junit.QuarkusIntegrationTest;
import io.restassured.RestAssured;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
+import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.parser.OpenAPIV3Parser;
import io.swagger.v3.parser.core.models.ParseOptions;
import io.swagger.v3.parser.core.models.SwaggerParseResult;
@@ -60,4 +61,24 @@ class OASIT {
assertThat(p1.getPost()).isNotNull();
}
+ @Test
+ public void testApprovalsSchemaProperties() {
+ String url = rootUrl.toString() + "/q/openapi";
+ ParseOptions parseOptions = new ParseOptions();
+ parseOptions.setResolve(true);
+ SwaggerParseResult result = new OpenAPIV3Parser().readLocation(url,
null, parseOptions);
+
+ OpenAPI openAPI = result.getOpenAPI();
+ PathItem p1 = openAPI.getPaths().get("/approvals");
+ assertThat(p1).isNotNull();
+
+ Schema requestSchema =
p1.getPost().getRequestBody().getContent().get("application/json").getSchema();
+ String schemaName =
requestSchema.get$ref().substring(requestSchema.get$ref().lastIndexOf('/') + 1);
+ Schema resolvedSchema =
openAPI.getComponents().getSchemas().get(schemaName);
+ assertThat(resolvedSchema).isNotNull();
+
+ assertThat(resolvedSchema.getProperties().keySet())
+ .containsExactlyInAnyOrder("approver", "traveller",
"firstLineApproval", "secondLineApproval");
+ }
+
}
diff --git
a/springboot/integration-tests/integration-tests-springboot-processes-it/src/main/resources/AdHocFragments.bpmn
b/springboot/integration-tests/integration-tests-springboot-processes-it/src/main/resources/AdHocFragments.bpmn
index a9bdf9430c..02675c1d61 100644
---
a/springboot/integration-tests/integration-tests-springboot-processes-it/src/main/resources/AdHocFragments.bpmn
+++
b/springboot/integration-tests/integration-tests-springboot-processes-it/src/main/resources/AdHocFragments.bpmn
@@ -19,6 +19,7 @@
-->
<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:bpsim="http://www.bpsim.org/schemas/1.0"
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
xmlns:drools="http://www.jboss.org/drools" id="_WL3jkCdwED2KYMa5towL6A"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd
http://www. [...]
<bpmn2:itemDefinition id="_var1Item" structureRef="String"/>
+ <bpmn2:itemDefinition id="_var2Item" structureRef="Integer"/>
<bpmn2:itemDefinition
id="__731518E6-88A2-4CCC-A2C0-97C45189A7F3_ConditionInputXItem"
structureRef="String"/>
<bpmn2:itemDefinition
id="__17762652-67B8-46FF-ADFE-D7AF3EA2ADAB_SkippableInputXItem"
structureRef="Object"/>
<bpmn2:itemDefinition
id="__17762652-67B8-46FF-ADFE-D7AF3EA2ADAB_PriorityInputXItem"
structureRef="Object"/>
@@ -64,6 +65,7 @@
</bpmn2:collaboration>
<bpmn2:process id="TestCase.AdHocFragments"
drools:packageName="org.kie.kogito" drools:version="1.0" drools:adHoc="true"
name="AdHocFragments" isExecutable="true" processType="Public">
<bpmn2:property id="var1" itemSubjectRef="_var1Item" name="var1"/>
+ <bpmn2:property id="var2" itemSubjectRef="_var2Item" name="var2"/>
<bpmn2:sequenceFlow id="_830D53F8-A344-45AE-A1A1-2173DD983A43"
sourceRef="_89FFDFF9-8D8A-4B86-B48C-76F30299D8D3"
targetRef="_319B612D-CD70-4FB0-8390-2F825115847E">
<bpmn2:extensionElements>
<drools:metaData name="isAutoConnection.source">
diff --git
a/springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/BasicRestTest.java
b/springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/BasicRestTest.java
index db13274bf8..c4655363d4 100644
---
a/springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/BasicRestTest.java
+++
b/springboot/integration-tests/integration-tests-springboot-processes-it/src/test/java/org/kie/kogito/integrationtests/springboot/BasicRestTest.java
@@ -38,6 +38,7 @@ import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.emptyOrNullString;
+import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = KogitoSpringbootApplication.class)
@@ -207,6 +208,164 @@ class BasicRestTest extends BaseRestTest {
assertExpectedUnitOfWorkEvents(2);
}
+ @Test
+ void testUpdateNullsOmittedField() {
+ Map<String, Object> params = new HashMap<>();
+ params.put("var1", "Kermit");
+ params.put("var2", 34);
+
+ String id = given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body(params)
+ .post("/AdHocFragments")
+ .then()
+ .statusCode(201)
+ .header("Location", not(emptyOrNullString()))
+ .body("id", not(emptyOrNullString()))
+ .body("var1", equalTo("Kermit"))
+ .body("var2", equalTo(34))
+ .extract()
+ .path("id");
+
+ // var2 omitted from PUT body -> full replace must null it
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body("{\"var1\": \"Gonzo\"}")
+ .put("/AdHocFragments/{customId}", id)
+ .then()
+ .statusCode(200)
+ .body("id", equalTo(id))
+ .body("var1", equalTo("Gonzo"))
+ .body("var2", nullValue());
+
+ assertExpectedUnitOfWorkEvents(2);
+ }
+
+ @Test
+ void testPatch() {
+ Map<String, String> params = new HashMap<>();
+ params.put("var1", "Kermit");
+
+ String id = given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body(params)
+ .post("/AdHocFragments")
+ .then()
+ .statusCode(201)
+ .header("Location", not(emptyOrNullString()))
+ .body("id", not(emptyOrNullString()))
+ .body("var1", equalTo("Kermit"))
+ .extract()
+ .path("id");
+
+ // An empty PATCH body must leave existing variables untouched.
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body("{}")
+ .patch("/AdHocFragments/{customId}", id)
+ .then()
+ .statusCode(200)
+ .body("id", equalTo(id))
+ .body("var1", equalTo("Kermit"));
+
+ assertExpectedUnitOfWorkEvents(2);
+ }
+
+ @Test
+ void testPatchWithNullValue() {
+ Map<String, String> params = new HashMap<>();
+ params.put("var1", "Kermit");
+
+ String id = given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body(params)
+ .post("/AdHocFragments")
+ .then()
+ .statusCode(201)
+ .header("Location", not(emptyOrNullString()))
+ .body("id", not(emptyOrNullString()))
+ .body("var1", equalTo("Kermit"))
+ .extract()
+ .path("id");
+
+ // var1 set to null must be cleared, unlike an omitted field
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body("{\"var1\": null}")
+ .patch("/AdHocFragments/{customId}", id)
+ .then()
+ .statusCode(200)
+ .body("id", equalTo(id))
+ .body("var1", nullValue());
+
+ assertExpectedUnitOfWorkEvents(2);
+ }
+
+ @Test
+ void testPatchAndPutSequence() {
+ Map<String, Object> params = new HashMap<>();
+ params.put("var1", "Kermit");
+ params.put("var2", 34);
+
+ String id = given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body(params)
+ .post("/AdHocFragments")
+ .then()
+ .statusCode(201)
+ .header("Location", not(emptyOrNullString()))
+ .body("id", not(emptyOrNullString()))
+ .body("var1", equalTo("Kermit"))
+ .body("var2", equalTo(34))
+ .extract()
+ .path("id");
+
+ // PATCH: var1 explicitly nulled, var2 untouched
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body("{\"var1\": null}")
+ .patch("/AdHocFragments/{customId}", id)
+ .then()
+ .statusCode(200)
+ .body("id", equalTo(id))
+ .body("var1", nullValue())
+ .body("var2", equalTo(34));
+
+ // PUT right after a PATCH on the same resource: var2 omitted -> must
still be nulled out
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body("{\"var1\": \"Gonzo\"}")
+ .put("/AdHocFragments/{customId}", id)
+ .then()
+ .statusCode(200)
+ .body("id", equalTo(id))
+ .body("var1", equalTo("Gonzo"))
+ .body("var2", nullValue());
+
+ // PATCH right after a PUT: empty body must leave both fields untouched
+ given()
+ .contentType(ContentType.JSON)
+ .when()
+ .body("{}")
+ .patch("/AdHocFragments/{customId}", id)
+ .then()
+ .statusCode(200)
+ .body("id", equalTo(id))
+ .body("var1", equalTo("Gonzo"))
+ .body("var2", nullValue());
+
+ assertExpectedUnitOfWorkEvents(4);
+ }
+
@Test
void testDelete() {
Map<String, String> params = new HashMap<>();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]