From 02fadd7869aadfc07638d50f94798679bd856f27 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Fri, 16 May 2025 12:21:28 +0100 Subject: [PATCH 01/27] extended knative custom functions to support headers and query params Signed-off-by: Pedro Escaleira --- .../customfunctions/PlainJsonKnativeParamsDecorator.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java index 03d335934fb..1ddd8182649 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java @@ -23,20 +23,21 @@ import org.kie.kogito.event.cloudevents.utils.CloudEventUtils; import org.kie.kogito.internal.process.workitem.KogitoWorkItem; -import org.kogito.workitem.rest.decorators.ParamsDecorator; +import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; import io.vertx.mutiny.ext.web.client.HttpRequest; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.CLOUDEVENT_SENT_AS_PLAIN_JSON_ERROR_MESSAGE; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.ID; -public final class PlainJsonKnativeParamsDecorator implements ParamsDecorator { +public final class PlainJsonKnativeParamsDecorator extends PrefixParamsDecorator { @Override public void decorate(KogitoWorkItem workItem, Map parameters, HttpRequest request) { if (isCloudEvent(KnativeFunctionPayloadSupplier.getPayload(parameters))) { throw new IllegalArgumentException(CLOUDEVENT_SENT_AS_PLAIN_JSON_ERROR_MESSAGE); } + super.decorate(workItem, parameters, request); } private static boolean isCloudEvent(Map payload) { From 515c491238c561fa063127b63985ff17b6bc9ef9 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Fri, 23 May 2025 16:17:23 +0100 Subject: [PATCH 02/27] code for returning REST custom function headers, status code, and to not fail for error response status code when developers choose to do so Signed-off-by: Pedro Escaleira --- .../parser/types/RestTypeHandler.java | 9 ++- .../workitem/rest/RestWorkItemHandler.java | 13 +++- .../decorators/AbstractParamsDecorator.java | 32 ++++++---- .../decorators/PrefixParamsDecorator.java | 4 +- .../DefaultRestWorkItemHandlerResult.java | 60 ++++++++++++++++++- .../rest/RestWorkItemHandlerTest.java | 2 +- 6 files changed, 100 insertions(+), 20 deletions(-) diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-rest-parser/src/main/java/org/kie/kogito/serverless/workflow/parser/types/RestTypeHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-rest-parser/src/main/java/org/kie/kogito/serverless/workflow/parser/types/RestTypeHandler.java index 3904466a3df..e4b1148159a 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-rest-parser/src/main/java/org/kie/kogito/serverless/workflow/parser/types/RestTypeHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-rest-parser/src/main/java/org/kie/kogito/serverless/workflow/parser/types/RestTypeHandler.java @@ -44,6 +44,7 @@ public class RestTypeHandler extends WorkItemTypeHandler { public static final String REST_TYPE = "rest"; private static final String METHOD_SEPARATOR = ":"; private static final String PORT = "port"; + private static final String HOST = "host"; @Override protected > WorkItemNodeFactory fillWorkItemHandler(Workflow workflow, @@ -65,9 +66,15 @@ public class RestTypeHandler extends WorkItemTypeHandler { .workParameter(RestWorkItemHandler.METHOD, method) .workParameter(RestWorkItemHandler.USER, runtimeRestApi(functionDef, USER_PROP, context.getContext())) .workParameter(RestWorkItemHandler.PASSWORD, runtimeRestApi(functionDef, PASSWORD_PROP, context.getContext())) - .workParameter(RestWorkItemHandler.HOST, runtimeRestApi(functionDef, "host", context.getContext())) + .workParameter(RestWorkItemHandler.HOST, runtimeRestApi(functionDef, HOST, context.getContext())) .workParameter(RestWorkItemHandler.PORT, runtimeRestApi(functionDef, PORT, context.getContext(), Integer.class, context.getContext().getApplicationProperty(APP_PROPERTIES_FUNCTIONS_BASE + PORT).map(Integer::parseInt).orElse(null))) + .workParameter(RestWorkItemHandler.RETURN_HEADERS, runtimeRestApi(functionDef, RestWorkItemHandler.RETURN_HEADERS, context.getContext(), Boolean.class, + context.getContext().getApplicationProperty(APP_PROPERTIES_FUNCTIONS_BASE + RestWorkItemHandler.RETURN_HEADERS).map(Boolean::parseBoolean).orElse(false))) + .workParameter(RestWorkItemHandler.RETURN_STATUS_CODE, runtimeRestApi(functionDef, RestWorkItemHandler.RETURN_STATUS_CODE, context.getContext(), Boolean.class, + context.getContext().getApplicationProperty(APP_PROPERTIES_FUNCTIONS_BASE + RestWorkItemHandler.RETURN_STATUS_CODE).map(Boolean::parseBoolean).orElse(false))) + .workParameter(RestWorkItemHandler.FAIL_ON_STATUS_ERROR, runtimeRestApi(functionDef, RestWorkItemHandler.FAIL_ON_STATUS_ERROR, context.getContext(), Boolean.class, + context.getContext().getApplicationProperty(APP_PROPERTIES_FUNCTIONS_BASE + RestWorkItemHandler.FAIL_ON_STATUS_ERROR).map(Boolean::parseBoolean).orElse(true))) .workParameter(RestWorkItemHandler.BODY_BUILDER, new ParamsRestBodyBuilderSupplier()) .workParameter(BearerTokenAuthDecorator.BEARER_TOKEN, runtimeRestApi(functionDef, ACCESS_TOKEN, context.getContext())) .workParameter(ApiKeyAuthDecorator.KEY_PREFIX, runtimeRestApi(functionDef, API_KEY_PREFIX, context.getContext())) diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java index 48545be05e1..5d623a785b0 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java @@ -88,6 +88,9 @@ public class RestWorkItemHandler extends DefaultKogitoWorkItemHandler { public static final String PARAMS_DECORATOR = "ParamsDecorator"; public static final String PATH_PARAM_RESOLVER = "PathParamResolver"; public static final String AUTH_METHOD = "AuthMethod"; + public static final String RETURN_HEADERS = "return_headers"; + public static final String RETURN_STATUS_CODE = "return_status_code"; + public static final String FAIL_ON_STATUS_ERROR = "fail_on_status_error"; public static final String REQUEST_TIMEOUT_IN_MILLIS = "RequestTimeout"; @@ -95,7 +98,7 @@ public class RestWorkItemHandler extends DefaultKogitoWorkItemHandler { public static final int DEFAULT_SSL_PORT = 443; private static final Logger logger = LoggerFactory.getLogger(RestWorkItemHandler.class); - private static final RestWorkItemHandlerResult DEFAULT_RESULT_HANDLER = new DefaultRestWorkItemHandlerResult(); + private RestWorkItemHandlerResult DEFAULT_RESULT_HANDLER; private static final RestWorkItemHandlerBodyBuilder DEFAULT_BODY_BUILDER = new DefaultWorkItemHandlerBodyBuilder(); private static final ParamsDecorator DEFAULT_PARAMS_DECORATOR = new PrefixParamsDecorator(); private static final PathParamResolver DEFAULT_PATH_PARAM_RESOLVER = new DefaultPathParamResolver(); @@ -129,6 +132,12 @@ public Optional activateWorkItemHandler(KogitoWorkItemManage throw new IllegalArgumentException("Missing required parameter " + URL); } + boolean failOnStatusError = getParam(parameters, FAIL_ON_STATUS_ERROR, Boolean.class, true); + + boolean returnHeaders = getParam(parameters, RETURN_HEADERS, Boolean.class, false); + boolean returnStatusCode = getParam(parameters, RETURN_STATUS_CODE, Boolean.class, false); + DEFAULT_RESULT_HANDLER = new DefaultRestWorkItemHandlerResult(returnHeaders, returnStatusCode); + HttpMethod method = getParam(parameters, METHOD, HttpMethod.class, HttpMethod.GET); RestWorkItemHandlerResult resultHandler = getClassParam(parameters, RESULT_HANDLER, RestWorkItemHandlerResult.class, DEFAULT_RESULT_HANDLER, resultHandlers); RestWorkItemHandlerBodyBuilder bodyBuilder = getClassParam(parameters, BODY_BUILDER, RestWorkItemHandlerBodyBuilder.class, DEFAULT_BODY_BUILDER, bodyBuilders); @@ -188,7 +197,7 @@ public Optional activateWorkItemHandler(KogitoWorkItemManage ? sendJson(request, bodyBuilder.apply(parameters), requestTimeout) : send(request, requestTimeout); int statusCode = response.statusCode(); - if (statusCode < 200 || statusCode >= 300) { + if (failOnStatusError && (statusCode < 200 || statusCode >= 300)) { throw new WorkItemExecutionException(Integer.toString(statusCode), "Request for endpoint " + endPoint + " failed with message: " + response.statusMessage()); } diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java index c88916322e6..6509fc5304e 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java @@ -18,9 +18,11 @@ */ package org.kogito.workitem.rest.decorators; +import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import org.kie.kogito.internal.process.workitem.KogitoWorkItem; @@ -30,18 +32,7 @@ public abstract class AbstractParamsDecorator implements ParamsDecorator { @Override public void decorate(KogitoWorkItem item, Map parameters, HttpRequest request) { - Iterator> iter = parameters.entrySet().iterator(); - while (iter.hasNext()) { - Entry entry = iter.next(); - String key = entry.getKey(); - if (isHeaderParameter(key)) { - request.putHeader(toHeaderKey(key), entry.getValue().toString()); - iter.remove(); - } else if (isQueryParameter(key)) { - request.addQueryParam(toQueryKey(key), entry.getValue().toString()); - iter.remove(); - } - } + extractHeadersQueries(item, parameters, request); } protected String toHeaderKey(String key) { @@ -52,6 +43,23 @@ protected String toQueryKey(String key) { return key; } + protected Set extractHeadersQueries(KogitoWorkItem item, Map parameters, HttpRequest request) { + Set consideredParams = new HashSet<>(); + + Iterator> iter = parameters.entrySet().iterator(); + while (iter.hasNext()) { + Entry entry = iter.next(); + String key = entry.getKey(); + if (isHeaderParameter(key) || isQueryParameter(key)) { + request.putHeader(isHeaderParameter(key) ? toHeaderKey(key) : toQueryKey(key), entry.getValue().toString()); + consideredParams.add(key); + iter.remove(); + } + } + + return consideredParams; + } + protected abstract boolean isHeaderParameter(String key); protected abstract boolean isQueryParameter(String key); diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java index 1d88e84e9d9..191d08a9d76 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java @@ -20,8 +20,8 @@ public class PrefixParamsDecorator extends AbstractParamsDecorator { - private static final String HEADER_PREFIX = "HEADER_"; - private static final String QUERY_PREFIX = "QUERY_"; + public static final String HEADER_PREFIX = "HEADER_"; + public static final String QUERY_PREFIX = "QUERY_"; @Override protected boolean isHeaderParameter(String key) { diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java index b21e4e19956..9b8fc079cab 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java @@ -18,15 +18,71 @@ */ package org.kogito.workitem.rest.resulthandlers; +import java.util.HashMap; import java.util.Map; +import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; + +import com.fasterxml.jackson.databind.JsonNode; + +import io.vertx.core.json.DecodeException; import io.vertx.mutiny.core.buffer.Buffer; import io.vertx.mutiny.ext.web.client.HttpResponse; public class DefaultRestWorkItemHandlerResult implements RestWorkItemHandlerResult { + public static final String STATUS_CODE_PARAM = "STATUS_CODE"; + + private boolean returnHeaders = false; + private boolean returnStatusCode = false; + + public DefaultRestWorkItemHandlerResult(boolean returnHeaders, boolean returnStatusCode) { + this.returnHeaders = returnHeaders; + this.returnStatusCode = returnStatusCode; + } + @Override - public Object apply(HttpResponse response, Class target) { - return target == null ? response.bodyAsJson(Map.class) : response.bodyAsJson(target); + public Object apply(HttpResponse response, Class target) {; + Map result = new HashMap<>(); + + try { + Object body = target == null ? response.bodyAsJson(Map.class) : response.bodyAsJson(target); + + if (!this.returnHeaders && !this.returnStatusCode) { + return body; + } + + if (body instanceof Map) { + ((Map) body).forEach((key, value) -> result.put(String.valueOf(key), value)); + } else if (body instanceof JsonNode && ((JsonNode) body).isObject()) { + JsonNode node = (JsonNode) body; + node.fields().forEachRemaining(entry -> + result.put(entry.getKey(), extractJsonNodeValue(entry.getValue())) + ); + } else { + result.put("body", body); + } + } catch (DecodeException e) { + result.put("body", response.bodyAsString()); + } + + if (this.returnHeaders) { + response.headers().forEach(entry -> result.put(PrefixParamsDecorator.HEADER_PREFIX + entry.getKey(), entry.getValue())); + } + if (this.returnStatusCode) { + result.put(STATUS_CODE_PARAM, response.statusCode()); + } + + return result; + } + + private static Object extractJsonNodeValue(JsonNode node) { + if (node.isTextual()) return node.textValue(); + if (node.isInt()) return node.intValue(); + if (node.isLong()) return node.longValue(); + if (node.isDouble()) return node.doubleValue(); + if (node.isBoolean()) return node.booleanValue(); + if (node.isNull()) return null; + return node.toString(); } } diff --git a/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/RestWorkItemHandlerTest.java b/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/RestWorkItemHandlerTest.java index a67ddb814d0..81a6c81a111 100644 --- a/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/RestWorkItemHandlerTest.java +++ b/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/RestWorkItemHandlerTest.java @@ -157,7 +157,7 @@ public void init() { public void testEmptyInputModel() { ObjectMapper objectMapper = new ObjectMapper(); ObjectNode objectNode = objectMapper.createObjectNode().put("id", 26).put("name", "pepe"); - RestWorkItemHandlerResult resultHandler = new DefaultRestWorkItemHandlerResult(); + RestWorkItemHandlerResult resultHandler = new DefaultRestWorkItemHandlerResult(false, false); HttpResponse response = mock(HttpResponse.class); when(response.bodyAsJson(ObjectNode.class)).thenReturn(objectNode); assertThat(resultHandler.apply(response, ObjectNode.class)).isSameAs(objectNode); From f045a43e6abbe173c4c892218709788efa3c3f29 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Fri, 23 May 2025 18:37:44 +0100 Subject: [PATCH 03/27] code for returning Knative custom function headers, status code, and to not fail for error response status code when developers choose to do so Signed-off-by: Pedro Escaleira --- .../customfunctions/KnativeTypeHandler.java | 5 +- .../serving/customfunctions/Operation.java | 59 ++++++++++++++++++- .../PlainJsonKnativeParamsDecorator.java | 41 ++++++++++++- 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java b/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java index 69fdcce9255..88382ed3047 100644 --- a/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java +++ b/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java @@ -80,7 +80,10 @@ public class KnativeTypeHandler extends WorkItemTypeHandler { node.workParameter(KnativeWorkItemHandler.SERVICE_PROPERTY_NAME, operation.getService()) .workParameter(KnativeWorkItemHandler.PATH_PROPERTY_NAME, operation.getPath()) - .workParameter(RestWorkItemHandler.METHOD, operation.getHttpMethod()); + .workParameter(RestWorkItemHandler.METHOD, operation.getHttpMethod()) + .workParameter(RestWorkItemHandler.RETURN_HEADERS, operation.returnHeaders()) + .workParameter(RestWorkItemHandler.RETURN_STATUS_CODE, operation.returnStatusCode()) + .workParameter(RestWorkItemHandler.FAIL_ON_STATUS_ERROR, operation.failOnStatusError()); return addFunctionArgs(workflow, fillWorkItemHandler(workflow, context, node, functionDef), diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java index c5a431686ef..e9d6e566de1 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java @@ -37,6 +37,12 @@ public final class Operation { static final String PATH_PARAMETER_NAME = "path"; static final String METHOD_PARAMETER_NAME = "method"; + + static final String RETURN_HEADERS_PARAMETER_NAME = "returnHeaders"; + + static final String RETURN_STATUS_CODE_PARAMETER_NAME = "returnStatusCode"; + + static final String FAIL_ON_STATUS_ERROR_PARAMETER_NAME = "failOnStatusError"; private final String service; @@ -46,11 +52,20 @@ public final class Operation { private final HttpMethod httpMethod; + private final boolean returnHeaders; + + private final boolean returnStatusCode; + + private final boolean failOnStatusError; + private Operation(Builder builder) { this.service = Objects.requireNonNull(builder.service); this.path = builder.path != null ? builder.path : "/"; this.isCloudEvent = builder.isCloudEvent; this.httpMethod = builder.httpMethod; + this.returnHeaders = builder.returnHeaders; + this.returnStatusCode = builder.returnStatusCode; + this.failOnStatusError = builder.failOnStatusError; validate(this); } @@ -81,6 +96,18 @@ public HttpMethod getHttpMethod() { return httpMethod; } + public boolean returnHeaders() { + return returnHeaders; + } + + public boolean returnStatusCode() { + return returnStatusCode; + } + + public boolean failOnStatusError() { + return failOnStatusError; + } + public static Operation parse(String value) { String[] parts = value.split("\\?", 2); @@ -96,6 +123,9 @@ public static Operation parse(String value) { .withPath(params.get(PATH_PARAMETER_NAME)) .withIsCloudEvent(Boolean.parseBoolean(params.get(CLOUD_EVENT_PARAMETER_NAME))) .withMethod(HttpMethod.valueOf(params.getOrDefault(METHOD_PARAMETER_NAME, DEFAULT_HTTP_METHOD.name()).toUpperCase())) + .withReturnHeaders(Boolean.parseBoolean(params.get(RETURN_HEADERS_PARAMETER_NAME))) + .withReturnStatusCode(Boolean.parseBoolean(params.get(RETURN_STATUS_CODE_PARAMETER_NAME))) + .withFailOnStatusError(Boolean.parseBoolean(params.get(FAIL_ON_STATUS_ERROR_PARAMETER_NAME))) .build(); } @@ -113,6 +143,9 @@ public boolean equals(Object o) { } Operation operation = (Operation) o; return isCloudEvent == operation.isCloudEvent + && returnHeaders == operation.returnHeaders + && returnStatusCode == operation.returnStatusCode + && failOnStatusError == operation.failOnStatusError && Objects.equals(service, operation.service) && Objects.equals(path, operation.path) && Objects.equals(httpMethod, operation.httpMethod); @@ -125,12 +158,15 @@ public String toString() { ", path='" + path + '\'' + ", isCloudEvent=" + isCloudEvent + ", httpMethod=" + httpMethod + + ", returnHeaders=" + returnHeaders + + ", returnStatusCode=" + returnStatusCode + + ", failOnStatusError=" + failOnStatusError + '}'; } @Override public int hashCode() { - return Objects.hash(service, path, isCloudEvent, httpMethod); + return Objects.hash(service, path, isCloudEvent, httpMethod, returnHeaders, returnStatusCode, failOnStatusError); } public static class Builder { @@ -143,6 +179,12 @@ public static class Builder { private HttpMethod httpMethod = DEFAULT_HTTP_METHOD; + private boolean returnHeaders; + + private boolean returnStatusCode; + + private boolean failOnStatusError; + private Builder() { } @@ -166,6 +208,21 @@ public Builder withMethod(HttpMethod httpMethod) { return this; } + public Builder withReturnHeaders(boolean returnHeaders) { + this.returnHeaders = returnHeaders; + return this; + } + + public Builder withReturnStatusCode(boolean returnStatusCode) { + this.returnStatusCode = returnStatusCode; + return this; + } + + public Builder withFailOnStatusError(boolean failOnStatusError) { + this.failOnStatusError = failOnStatusError; + return this; + } + public Operation build() { return new Operation(this); } diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java index 1ddd8182649..0001de44dc3 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java @@ -18,17 +18,28 @@ */ package org.kie.kogito.addons.quarkus.knative.serving.customfunctions; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; import org.kie.kogito.event.cloudevents.utils.CloudEventUtils; import org.kie.kogito.internal.process.workitem.KogitoWorkItem; +import org.kie.kogito.jackson.utils.ObjectNodeListenerAware; +import org.kogito.workitem.rest.RestWorkItemHandler; import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; import io.vertx.mutiny.ext.web.client.HttpRequest; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.CLOUDEVENT_SENT_AS_PLAIN_JSON_ERROR_MESSAGE; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.ID; +import static org.kie.kogito.serverless.workflow.SWFConstants.MODEL_WORKFLOW_VAR; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; public final class PlainJsonKnativeParamsDecorator extends PrefixParamsDecorator { @@ -37,11 +48,39 @@ public void decorate(KogitoWorkItem workItem, Map parameters, Ht if (isCloudEvent(KnativeFunctionPayloadSupplier.getPayload(parameters))) { throw new IllegalArgumentException(CLOUDEVENT_SENT_AS_PLAIN_JSON_ERROR_MESSAGE); } - super.decorate(workItem, parameters, request); + buildFromParams(workItem, parameters, request); } private static boolean isCloudEvent(Map payload) { List cloudEventMissingAttributes = CloudEventUtils.getMissingAttributes(payload); return !payload.isEmpty() && (cloudEventMissingAttributes.isEmpty() || (cloudEventMissingAttributes.size() == 1 && cloudEventMissingAttributes.contains(ID))); } + + private void buildFromParams(KogitoWorkItem workItem, Map parameters, HttpRequest request) { + Map inputModel = new HashMap<>(); + + Object inputModelObject = parameters.get(MODEL_WORKFLOW_VAR); + if (inputModelObject != null && inputModelObject instanceof ObjectNodeListenerAware) { + ObjectMapper mapper = new ObjectMapper(); + ((ObjectNodeListenerAware) inputModelObject).fields().forEachRemaining(entry -> { + JsonNode value = entry.getValue(); + Object rawValue = mapper.convertValue(value, Object.class); + inputModel.put(entry.getKey(), rawValue); + }); + } + + Set keysFilter = Set.of(RestWorkItemHandler.REQUEST_TIMEOUT_IN_MILLIS, MODEL_WORKFLOW_VAR); + Map filteredParams = parameters.entrySet().stream() + .filter(entry -> !keysFilter.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + if (filteredParams.isEmpty()) { + Set paramsRemove = super.extractHeadersQueries(workItem, inputModel, request); + if (inputModelObject != null && inputModelObject instanceof ObjectNodeListenerAware) { + ((ObjectNodeListenerAware) inputModelObject).remove(paramsRemove); + } + } else { + super.decorate(workItem, parameters, request); + } + } } From ebde23356a51b4b271be09953539398777567f61 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Wed, 28 May 2025 17:52:15 +0100 Subject: [PATCH 04/27] added the header and query arguments feature in knative for POSTs as CloudEvents and GET requests; created integration tests accordingly Signed-off-by: Pedro Escaleira --- .../KogitoAddonKnativeServingProcessor.java | 3 +- .../customfunctions/KnativeTypeHandler.java | 9 +- .../cloudEventHeadersKnativeFunction.sw.json | 36 +++++ ...adersAndQueryParamsKnativeFunction.sw.json | 33 ++++ ...sAndQueryParamsKnativeFunctionPost.sw.json | 34 ++++ .../resources/headersKnativeFunction.sw.json | 33 ++++ .../queryParamsKnativeFunction.sw.json | 33 ++++ .../queryParamsKnativeFunctionPost.sw.json | 34 ++++ .../it/KnativeServingAddonIT.java | 150 ++++++++++++++++++ .../CloudEventKnativeParamsDecorator.java | 6 +- .../customfunctions/GetParamsDecorator.java | 52 ++++++ .../PlainJsonKnativeParamsDecorator.java | 1 + 12 files changed, 417 insertions(+), 7 deletions(-) create mode 100644 quarkus/addons/knative/serving/integration-tests/src/main/resources/cloudEventHeadersKnativeFunction.sw.json create mode 100644 quarkus/addons/knative/serving/integration-tests/src/main/resources/headersAndQueryParamsKnativeFunction.sw.json create mode 100644 quarkus/addons/knative/serving/integration-tests/src/main/resources/headersAndQueryParamsKnativeFunctionPost.sw.json create mode 100644 quarkus/addons/knative/serving/integration-tests/src/main/resources/headersKnativeFunction.sw.json create mode 100644 quarkus/addons/knative/serving/integration-tests/src/main/resources/queryParamsKnativeFunction.sw.json create mode 100644 quarkus/addons/knative/serving/integration-tests/src/main/resources/queryParamsKnativeFunctionPost.sw.json create mode 100644 quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/GetParamsDecorator.java diff --git a/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/KogitoAddonKnativeServingProcessor.java b/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/KogitoAddonKnativeServingProcessor.java index f11134f5bd6..0d6a7700626 100644 --- a/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/KogitoAddonKnativeServingProcessor.java +++ b/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/KogitoAddonKnativeServingProcessor.java @@ -20,6 +20,7 @@ package org.kie.kogito.addons.quarkus.knative.serving.deployment; import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.CloudEventKnativeParamsDecorator; +import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.GetParamsDecorator; import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.PlainJsonKnativeParamsDecorator; import org.kie.kogito.quarkus.addons.common.deployment.KogitoCapability; import org.kie.kogito.quarkus.addons.common.deployment.OneOfCapabilityKogitoAddOnProcessor; @@ -46,7 +47,7 @@ public ReflectiveClassBuildItem reflectiveClasses() { return new ReflectiveClassBuildItem(true, true, true, - CloudEventKnativeParamsDecorator.class, PlainJsonKnativeParamsDecorator.class); + CloudEventKnativeParamsDecorator.class, PlainJsonKnativeParamsDecorator.class, GetParamsDecorator.class); } } diff --git a/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java b/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java index 69fdcce9255..f369b3c61ad 100644 --- a/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java +++ b/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java @@ -27,6 +27,7 @@ import org.jbpm.ruleflow.core.factory.NodeFactory; import org.jbpm.ruleflow.core.factory.WorkItemNodeFactory; import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.CloudEventKnativeParamsDecorator; +import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.GetParamsDecorator; import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler; import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation; import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.PlainJsonKnativeParamsDecorator; @@ -65,12 +66,12 @@ public class KnativeTypeHandler extends WorkItemTypeHandler { Operation operation = Operation.parse(trimCustomOperation(functionDef)); + if (!payloadFields.isEmpty()) { + node.workParameter(PAYLOAD_FIELDS_PROPERTY_NAME, payloadFields); + } if (HttpMethod.GET.equals(operation.getHttpMethod())) { - node.workParameter(RestWorkItemHandler.PARAMS_DECORATOR, new CollectionParamsDecoratorSupplier(List.of(), payloadFields)); + node.workParameter(RestWorkItemHandler.PARAMS_DECORATOR, GetParamsDecorator.class.getName()); } else { - if (!payloadFields.isEmpty()) { - node.workParameter(PAYLOAD_FIELDS_PROPERTY_NAME, payloadFields); - } if (operation.isCloudEvent()) { node.workParameter(RestWorkItemHandler.PARAMS_DECORATOR, CloudEventKnativeParamsDecorator.class.getName()); } else { diff --git a/quarkus/addons/knative/serving/integration-tests/src/main/resources/cloudEventHeadersKnativeFunction.sw.json b/quarkus/addons/knative/serving/integration-tests/src/main/resources/cloudEventHeadersKnativeFunction.sw.json new file mode 100644 index 00000000000..9626cb97aff --- /dev/null +++ b/quarkus/addons/knative/serving/integration-tests/src/main/resources/cloudEventHeadersKnativeFunction.sw.json @@ -0,0 +1,36 @@ +{ + "id": "cloudEventHeadersKnativeFunction", + "version": "1.0", + "name": "Test Knative function", + "description": "This workflow tests a Knative function", + "start": "invokeFunction", + "functions": [ + { + "name": "greet", + "type": "custom", + "operation": "knative:services.v1.serving.knative.dev/default/serverless-workflow-greeting-quarkus?path=/cloud-event&method=POST&asCloudEvent=true" + } + ], + "states": [ + { + "name": "invokeFunction", + "type": "operation", + "actions": [ + { + "functionRef": { + "refName": "greet", + "arguments": { + "specversion": "1.0", + "id": "42", + "source": "https://localhost:8080", + "type": "org.kie.kogito.test", + "HEADER_Test": "test", + "HEADER_Authorization": "Bearer token" + } + } + } + ], + "end": true + } + ] +} diff --git a/quarkus/addons/knative/serving/integration-tests/src/main/resources/headersAndQueryParamsKnativeFunction.sw.json b/quarkus/addons/knative/serving/integration-tests/src/main/resources/headersAndQueryParamsKnativeFunction.sw.json new file mode 100644 index 00000000000..c8b1fc99f23 --- /dev/null +++ b/quarkus/addons/knative/serving/integration-tests/src/main/resources/headersAndQueryParamsKnativeFunction.sw.json @@ -0,0 +1,33 @@ +{ + "id": "headersAndQueryParamsKnativeFunction", + "version": "1.0", + "name": "Test Knative function", + "description": "This workflow tests a Knative function", + "start": "invokeFunction", + "functions": [ + { + "name": "greet", + "type": "custom", + "operation": "knative:services.v1.serving.knative.dev/default/serverless-workflow-greeting-quarkus?path=/headersAndQueryParamsFunction&method=GET" + } + ], + "states": [ + { + "name": "invokeFunction", + "type": "operation", + "actions": [ + { + "functionRef": { + "refName": "greet", + "arguments": { + "QUERY_param1": "value1", + "HEADER_Authorization": "Bearer token" + } + } + } + ], + "end": true + } + ] +} + \ No newline at end of file diff --git a/quarkus/addons/knative/serving/integration-tests/src/main/resources/headersAndQueryParamsKnativeFunctionPost.sw.json b/quarkus/addons/knative/serving/integration-tests/src/main/resources/headersAndQueryParamsKnativeFunctionPost.sw.json new file mode 100644 index 00000000000..6eec5a5da7f --- /dev/null +++ b/quarkus/addons/knative/serving/integration-tests/src/main/resources/headersAndQueryParamsKnativeFunctionPost.sw.json @@ -0,0 +1,34 @@ +{ + "id": "headersAndQueryParamsKnativeFunctionPost", + "version": "1.0", + "name": "Test Knative function", + "description": "This workflow tests a Knative function", + "start": "invokeFunction", + "functions": [ + { + "name": "greet", + "type": "custom", + "operation": "knative:services.v1.serving.knative.dev/default/serverless-workflow-greeting-quarkus?path=/headersAndQueryParamsFunction&method=POST" + } + ], + "states": [ + { + "name": "invokeFunction", + "type": "operation", + "actions": [ + { + "functionRef": { + "refName": "greet", + "arguments": { + "QUERY_param1": "value1", + "HEADER_Authorization": "Bearer token", + "param2": "value2" + } + } + } + ], + "end": true + } + ] +} + \ No newline at end of file diff --git a/quarkus/addons/knative/serving/integration-tests/src/main/resources/headersKnativeFunction.sw.json b/quarkus/addons/knative/serving/integration-tests/src/main/resources/headersKnativeFunction.sw.json new file mode 100644 index 00000000000..1378d596530 --- /dev/null +++ b/quarkus/addons/knative/serving/integration-tests/src/main/resources/headersKnativeFunction.sw.json @@ -0,0 +1,33 @@ +{ + "id": "headersKnativeFunction", + "version": "1.0", + "name": "Test Knative function", + "description": "This workflow tests a Knative function", + "start": "invokeFunction", + "functions": [ + { + "name": "greet", + "type": "custom", + "operation": "knative:services.v1.serving.knative.dev/default/serverless-workflow-greeting-quarkus?path=/headersFunction&method=POST" + } + ], + "states": [ + { + "name": "invokeFunction", + "type": "operation", + "actions": [ + { + "functionRef": { + "refName": "greet", + "arguments": { + "HEADER_Test": "test", + "HEADER_Authorization": "Bearer token" + } + } + } + ], + "end": true + } + ] +} + \ No newline at end of file diff --git a/quarkus/addons/knative/serving/integration-tests/src/main/resources/queryParamsKnativeFunction.sw.json b/quarkus/addons/knative/serving/integration-tests/src/main/resources/queryParamsKnativeFunction.sw.json new file mode 100644 index 00000000000..190a4bccaaa --- /dev/null +++ b/quarkus/addons/knative/serving/integration-tests/src/main/resources/queryParamsKnativeFunction.sw.json @@ -0,0 +1,33 @@ +{ + "id": "queryParamsKnativeFunction", + "version": "1.0", + "name": "Test Knative function", + "description": "This workflow tests a Knative function", + "start": "invokeFunction", + "functions": [ + { + "name": "greet", + "type": "custom", + "operation": "knative:services.v1.serving.knative.dev/default/serverless-workflow-greeting-quarkus?path=/queryParamsFunction&method=GET" + } + ], + "states": [ + { + "name": "invokeFunction", + "type": "operation", + "actions": [ + { + "functionRef": { + "refName": "greet", + "arguments": { + "param1": "value1", + "param2": "value2", + "QUERY_param3": "value3" + } + } + } + ], + "end": true + } + ] +} diff --git a/quarkus/addons/knative/serving/integration-tests/src/main/resources/queryParamsKnativeFunctionPost.sw.json b/quarkus/addons/knative/serving/integration-tests/src/main/resources/queryParamsKnativeFunctionPost.sw.json new file mode 100644 index 00000000000..686b23373b8 --- /dev/null +++ b/quarkus/addons/knative/serving/integration-tests/src/main/resources/queryParamsKnativeFunctionPost.sw.json @@ -0,0 +1,34 @@ +{ + "id": "queryParamsKnativeFunctionPost", + "version": "1.0", + "name": "Test Knative function", + "description": "This workflow tests a Knative function", + "start": "invokeFunction", + "functions": [ + { + "name": "greet", + "type": "custom", + "operation": "knative:services.v1.serving.knative.dev/default/serverless-workflow-greeting-quarkus?path=/queryParamsFunction&method=POST" + } + ], + "states": [ + { + "name": "invokeFunction", + "type": "operation", + "actions": [ + { + "functionRef": { + "refName": "greet", + "arguments": { + "QUERY_param1": "value1", + "QUERY_param2": "value2", + "param3": "value3" + } + } + } + ], + "end": true + } + ] +} + \ No newline at end of file diff --git a/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java b/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java index 0e8686197eb..3d9562f6c9c 100644 --- a/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java +++ b/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java @@ -282,6 +282,87 @@ void executeTimeout() { .statusCode(HttpURLConnection.HTTP_INTERNAL_ERROR); } + @Test + void executeWithHeadersAsPlainJson() { + mockExecuteWithHeadersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/headersKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Headers added successfully")); + } + + @Test + void executeWithQueryParametersAsPlainJson() { + mockExecuteWithQueryParametersPostEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/queryParamsKnativeFunctionPost") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Query parameters added successfully")); + } + + @Test + void executeWithHeadersAndQueryParametersAsPlainJson() { + mockExecuteWithHeadersAndQueryParametersPostEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/headersAndQueryParamsKnativeFunctionPost") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Headers and query parameters added successfully")); + } + + @Test + void executeWithQueryParametersGet() { + mockExecuteWithQueryParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/queryParamsKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Query parameters added successfully")); + } + + @Test + void executeWithHeadersAndQueryParametersGet() { + mockExecuteWithHeadersAndQueryParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/headersAndQueryParamsKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Headers and query parameters added successfully")); + } + + @Test + void executeWithHeadersAsCloudEvent() { + mockExecuteWithHeadersCloudEventEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/cloudEventHeadersKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("CloudEvents with headers are awesome!")) + .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto").toPrettyString())); + } + private void mockExecuteTimeoutEndpoint() { wireMockServer.stubFor(post(urlEqualTo("/timeout")) .willReturn(aResponse() @@ -349,4 +430,73 @@ private void mockExecuteWithArrayEndpoint() { .withJsonBody(JsonNodeFactory.instance.objectNode() .put("message", JsonNodeFactory.instance.arrayNode().add(23).add(24).toPrettyString())))); } + + private void mockExecuteWithHeadersEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/headersFunction")) + .withHeader("Test", equalTo("test")) + .withHeader("Authorization", equalTo("Bearer token")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Headers added successfully")))); + } + + private void mockExecuteWithQueryParametersPostEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/queryParamsFunction?param2=value2¶m1=value1")) + .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() + .put("param3", "value3") + .toString())) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Query parameters added successfully")))); + } + + private void mockExecuteWithHeadersAndQueryParametersPostEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/headersAndQueryParamsFunction?param1=value1")) + .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() + .put("param2", "value2") + .toString())) + .withHeader("Authorization", equalTo("Bearer token")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Headers and query parameters added successfully")))); + } + + private void mockExecuteWithQueryParametersEndpoint() { + wireMockServer.stubFor(get(urlEqualTo("/queryParamsFunction?QUERY_param3=value3¶m1=value1¶m2=value2")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Query parameters added successfully")))); + } + + private void mockExecuteWithHeadersAndQueryParametersEndpoint() { + wireMockServer.stubFor(get(urlEqualTo("/headersAndQueryParamsFunction?param1=value1")) + .withHeader("Authorization", equalTo("Bearer token")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Headers and query parameters added successfully")))); + } + + private void mockExecuteWithHeadersCloudEventEndpoint() { + wireMockServer.stubFor(post(urlEqualTo(CLOUD_EVENT_PATH)) + .withHeader("Test", equalTo("test")) + .withHeader("Authorization", equalTo("Bearer token")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8) + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "CloudEvents with headers are awesome!") + .put("object", JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto").toPrettyString())))); + } } diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/CloudEventKnativeParamsDecorator.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/CloudEventKnativeParamsDecorator.java index 7efcecd2e50..34cfacf18ce 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/CloudEventKnativeParamsDecorator.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/CloudEventKnativeParamsDecorator.java @@ -22,14 +22,14 @@ import org.kie.kogito.event.cloudevents.utils.CloudEventUtils; import org.kie.kogito.internal.process.workitem.KogitoWorkItem; -import org.kogito.workitem.rest.decorators.ParamsDecorator; +import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; import io.vertx.mutiny.ext.web.client.HttpRequest; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.ID; -public final class CloudEventKnativeParamsDecorator implements ParamsDecorator { +public final class CloudEventKnativeParamsDecorator extends PrefixParamsDecorator { @Override public void decorate(KogitoWorkItem workItem, Map parameters, HttpRequest request) { @@ -44,6 +44,8 @@ public void decorate(KogitoWorkItem workItem, Map parameters, Ht CloudEventUtils.validateCloudEvent(cloudEvent); request.putHeader("Content-Type", APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8); + + super.decorate(workItem, parameters, request); } private static String generateCloudEventId(int uniqueIdentifier, String processInstanceId) { diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/GetParamsDecorator.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/GetParamsDecorator.java new file mode 100644 index 00000000000..f1a8b533272 --- /dev/null +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/GetParamsDecorator.java @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.kie.kogito.addons.quarkus.knative.serving.customfunctions; + +import java.util.Map; + +import org.kie.kogito.internal.process.workitem.KogitoWorkItem; +import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; + +import io.vertx.mutiny.ext.web.client.HttpRequest; + +public final class GetParamsDecorator extends PrefixParamsDecorator { + + private Map getParams; + + @Override + public void decorate(KogitoWorkItem item, Map parameters, HttpRequest request) { + this.getParams = KnativeFunctionPayloadSupplier.getPayload(parameters); + super.decorate(item, parameters, request); + } + + @Override + protected boolean isQueryParameter(String key) { + return this.getParams.containsKey(key) && !super.isHeaderParameter(key); + } + + @Override + protected boolean isHeaderParameter(String key) { + return this.getParams.containsKey(key) ? super.isHeaderParameter(key) : false; + } + + @Override + protected String toQueryKey(String key) { + return key; + } +} diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java index 1ddd8182649..57ac0c33d14 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java @@ -37,6 +37,7 @@ public void decorate(KogitoWorkItem workItem, Map parameters, Ht if (isCloudEvent(KnativeFunctionPayloadSupplier.getPayload(parameters))) { throw new IllegalArgumentException(CLOUDEVENT_SENT_AS_PLAIN_JSON_ERROR_MESSAGE); } + super.decorate(workItem, parameters, request); } From c1f4e7a3cd9f047b13e5127bd8825ce487a02c85 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Thu, 29 May 2025 10:40:01 +0100 Subject: [PATCH 05/27] small fix on the AbstractParamsDecorator Signed-off-by: Pedro Escaleira --- .../workitem/rest/decorators/AbstractParamsDecorator.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java index 6509fc5304e..f51d2e5ae81 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java @@ -50,8 +50,12 @@ protected Set extractHeadersQueries(KogitoWorkItem item, Map entry = iter.next(); String key = entry.getKey(); - if (isHeaderParameter(key) || isQueryParameter(key)) { - request.putHeader(isHeaderParameter(key) ? toHeaderKey(key) : toQueryKey(key), entry.getValue().toString()); + if (isHeaderParameter(key)) { + request.putHeader(toHeaderKey(key), entry.getValue().toString()); + consideredParams.add(key); + iter.remove(); + } else if (isQueryParameter(key)) { + request.addQueryParam(toQueryKey(key), entry.getValue().toString()); consideredParams.add(key); iter.remove(); } From 172886766c18046ca640c3bd54c21be386a52749 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Thu, 29 May 2025 15:12:37 +0100 Subject: [PATCH 06/27] fixed style Signed-off-by: Pedro Escaleira --- .../DefaultRestWorkItemHandlerResult.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java index 9b8fc079cab..9c1ef61cd38 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java @@ -42,7 +42,8 @@ public DefaultRestWorkItemHandlerResult(boolean returnHeaders, boolean returnSta } @Override - public Object apply(HttpResponse response, Class target) {; + public Object apply(HttpResponse response, Class target) { + ; Map result = new HashMap<>(); try { @@ -51,14 +52,12 @@ public DefaultRestWorkItemHandlerResult(boolean returnHeaders, boolean returnSta if (!this.returnHeaders && !this.returnStatusCode) { return body; } - + if (body instanceof Map) { ((Map) body).forEach((key, value) -> result.put(String.valueOf(key), value)); } else if (body instanceof JsonNode && ((JsonNode) body).isObject()) { JsonNode node = (JsonNode) body; - node.fields().forEachRemaining(entry -> - result.put(entry.getKey(), extractJsonNodeValue(entry.getValue())) - ); + node.fields().forEachRemaining(entry -> result.put(entry.getKey(), extractJsonNodeValue(entry.getValue()))); } else { result.put("body", body); } @@ -77,12 +76,18 @@ public DefaultRestWorkItemHandlerResult(boolean returnHeaders, boolean returnSta } private static Object extractJsonNodeValue(JsonNode node) { - if (node.isTextual()) return node.textValue(); - if (node.isInt()) return node.intValue(); - if (node.isLong()) return node.longValue(); - if (node.isDouble()) return node.doubleValue(); - if (node.isBoolean()) return node.booleanValue(); - if (node.isNull()) return null; + if (node.isTextual()) + return node.textValue(); + if (node.isInt()) + return node.intValue(); + if (node.isLong()) + return node.longValue(); + if (node.isDouble()) + return node.doubleValue(); + if (node.isBoolean()) + return node.booleanValue(); + if (node.isNull()) + return null; return node.toString(); } } From 08ff33d0641574384d0e9f97f0a2c623b950671b Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Thu, 29 May 2025 15:16:37 +0100 Subject: [PATCH 07/27] fixed style Signed-off-by: Pedro Escaleira --- .../quarkus/knative/serving/customfunctions/Operation.java | 2 +- .../customfunctions/PlainJsonKnativeParamsDecorator.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java index e9d6e566de1..952d302dc77 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java @@ -37,7 +37,7 @@ public final class Operation { static final String PATH_PARAMETER_NAME = "path"; static final String METHOD_PARAMETER_NAME = "method"; - + static final String RETURN_HEADERS_PARAMETER_NAME = "returnHeaders"; static final String RETURN_STATUS_CODE_PARAMETER_NAME = "returnStatusCode"; diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java index 0001de44dc3..56d34817a0a 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java @@ -71,8 +71,8 @@ private void buildFromParams(KogitoWorkItem workItem, Map parame Set keysFilter = Set.of(RestWorkItemHandler.REQUEST_TIMEOUT_IN_MILLIS, MODEL_WORKFLOW_VAR); Map filteredParams = parameters.entrySet().stream() - .filter(entry -> !keysFilter.contains(entry.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + .filter(entry -> !keysFilter.contains(entry.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); if (filteredParams.isEmpty()) { Set paramsRemove = super.extractHeadersQueries(workItem, inputModel, request); From f340c45e7d18b999bd64540976120aa8853597f3 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Thu, 29 May 2025 16:40:39 +0100 Subject: [PATCH 08/27] FAIL_ON_STATUS_ERROR_PARAMETER_NAME default for knative Signed-off-by: Pedro Escaleira --- .../quarkus/knative/serving/customfunctions/Operation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java index 952d302dc77..3dfd49fda74 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java @@ -125,7 +125,7 @@ public static Operation parse(String value) { .withMethod(HttpMethod.valueOf(params.getOrDefault(METHOD_PARAMETER_NAME, DEFAULT_HTTP_METHOD.name()).toUpperCase())) .withReturnHeaders(Boolean.parseBoolean(params.get(RETURN_HEADERS_PARAMETER_NAME))) .withReturnStatusCode(Boolean.parseBoolean(params.get(RETURN_STATUS_CODE_PARAMETER_NAME))) - .withFailOnStatusError(Boolean.parseBoolean(params.get(FAIL_ON_STATUS_ERROR_PARAMETER_NAME))) + .withFailOnStatusError(Boolean.parseBoolean(params.getOrDefault(FAIL_ON_STATUS_ERROR_PARAMETER_NAME, "true"))) .build(); } From 0d272276b94725c7af04c3c7e8b3d2eb49af21f5 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Thu, 29 May 2025 16:56:33 +0100 Subject: [PATCH 09/27] fixed failing tests and format Signed-off-by: Pedro Escaleira --- .../customfunctions/KnativeTypeHandler.java | 1 - ...ogitoAddonKnativeServingProcessorTest.java | 3 +- .../it/KnativeServingAddonIT.java | 888 +++++++++--------- 3 files changed, 446 insertions(+), 446 deletions(-) diff --git a/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java b/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java index f369b3c61ad..d52826af28e 100644 --- a/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java +++ b/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java @@ -34,7 +34,6 @@ import org.kie.kogito.serverless.workflow.parser.ParserContext; import org.kie.kogito.serverless.workflow.parser.VariableInfo; import org.kie.kogito.serverless.workflow.parser.types.WorkItemTypeHandler; -import org.kie.kogito.serverless.workflow.suppliers.CollectionParamsDecoratorSupplier; import org.kie.kogito.serverless.workflow.suppliers.ParamsRestBodyBuilderSupplier; import org.kogito.workitem.rest.RestWorkItemHandler; diff --git a/quarkus/addons/knative/serving/deployment/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/KogitoAddonKnativeServingProcessorTest.java b/quarkus/addons/knative/serving/deployment/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/KogitoAddonKnativeServingProcessorTest.java index f76c2d3edd6..3e46c4468fb 100644 --- a/quarkus/addons/knative/serving/deployment/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/KogitoAddonKnativeServingProcessorTest.java +++ b/quarkus/addons/knative/serving/deployment/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/KogitoAddonKnativeServingProcessorTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.CloudEventKnativeParamsDecorator; +import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.GetParamsDecorator; import org.kie.kogito.addons.quarkus.knative.serving.customfunctions.PlainJsonKnativeParamsDecorator; import static org.assertj.core.api.Assertions.assertThat; @@ -40,6 +41,6 @@ void reflectiveClasses() { assertThat(processor.reflectiveClasses()).isNotNull(); assertThat(processor.reflectiveClasses().getClassNames()) .containsExactlyInAnyOrder(CloudEventKnativeParamsDecorator.class.getName(), - PlainJsonKnativeParamsDecorator.class.getName()); + PlainJsonKnativeParamsDecorator.class.getName(), GetParamsDecorator.class.getName()); } } diff --git a/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java b/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java index 3d9562f6c9c..1328f026012 100644 --- a/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java +++ b/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java @@ -55,448 +55,448 @@ @WithKubernetesTestServer class KnativeServingAddonIT { - public static final String AT_LEAST_ONE_NON_WHITE_CHARACTER_REGEX = ".*\\S.*"; - private static final String NAMESPACE = "default"; - private static final String SERVICENAME = "serverless-workflow-greeting-quarkus"; - private static final String CLOUD_EVENT_PATH = "/cloud-event"; - private static WireMockServer wireMockServer; - - private static String remoteServiceUrl; - - @ConfigProperty(name = "kogito.sw.functions.greet_with_timeout.timeout") - Long requestTimeout; - - @KubernetesTestServer - KubernetesServer mockServer; - - @BeforeAll - static void beforeAll() { - createWiremockServer(); - } - - @AfterAll - static void afterAll() { - if (wireMockServer != null) { - wireMockServer.stop(); - } - } - - private static void createWiremockServer() { - wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); - wireMockServer.start(); - remoteServiceUrl = wireMockServer.baseUrl(); - } - - @BeforeEach - void beforeEach() { - createKnativeServiceIfNotExists(mockServer.getClient(), "knative/quarkus-greeting.yaml", NAMESPACE, SERVICENAME, remoteServiceUrl); - } - - @Test - void executeHttpGet() { - mockExecuteHttpGetEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .body("{\"name\": \"hbelmiro\" }").when() - .post("/getKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Hello")); - - wireMockServer.verify(getRequestedFor(urlEqualTo("/plainJsonFunction?name=hbelmiro"))); - } - - @Test - void executeWithEmptyParameters() { - mockExecuteWithEmptyParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .body("{\"workflowdata\":{}}").when() - .post("/emptyParamsKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.org", is("Acme")) - .body("workflowdata.project", is("Kogito")); - } - - @Test - void executeWithParameters() { - mockExecuteWithParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .body("{\"name\": \"hbelmiro\" }").when() - .post("/plainJsonKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Hello")); - } - - @Test - void executeWithArray() { - mockExecuteWithArrayEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/arrayKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is(JsonNodeFactory.instance.arrayNode().add(23).add(24).toPrettyString())); - } - - @Test - void executeWithParametersShouldSendOnlyFunctionArgs() { - mockExecuteWithParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .body("{\"name\": \"hbelmiro\", \"should_not_be_sent\" : \"value\" }").when() - .post("/plainJsonKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Hello")); - } - - @Test - void executeWithCloudEventWithIdAsPlainJson() { - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/cloudEventWithIdAsPlainJson") - .then() - .statusCode(HttpURLConnection.HTTP_BAD_REQUEST); - } - - @Test - void executeWithCloudEventWithoutIdAsPlainJson() { - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/cloudEventWithoutIdAsPlainJson") - .then() - .statusCode(HttpURLConnection.HTTP_BAD_REQUEST); - } - - @Test - void executeCloudEvent() { - mockExecuteCloudEventWithParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .post("/cloudEvent") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("CloudEvents are awesome!")) - .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() - .put("long", 42L) - .put("String", "xpto").toPrettyString())); - - wireMockServer.verify(postRequestedFor(urlEqualTo(CLOUD_EVENT_PATH)) - .withRequestBody(matchingJsonPath("$.id", equalTo("42"))) - .withHeader("Content-Type", equalTo(APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8))); - } - - @Test - void executeCloudEventWithMissingIdShouldNotThrowException() { - mockExecuteCloudEventWithParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .post("/cloudEventWithMissingId") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("CloudEvents are awesome!")) - .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() - .put("long", 42L) - .put("String", "xpto").toPrettyString())); - - wireMockServer.verify(postRequestedFor(urlEqualTo(CLOUD_EVENT_PATH)) - .withRequestBody(matchingJsonPath("$.id", WireMock.matching(AT_LEAST_ONE_NON_WHITE_CHARACTER_REGEX))) - .withHeader("Content-Type", equalTo(APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8))); - } - - @Test - void cloudEventWithIdMustBeSentAsIs() { - mockExecuteCloudEventWithParametersEndpoint(); - - String id = "42"; - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .body("{\"id\": \"" + id + "\" }").when() - .post("/cloudEventWithIdAsParam") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("CloudEvents are awesome!")) - .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() - .put("long", 42L) - .put("String", "xpto").toPrettyString())); - - wireMockServer.verify(postRequestedFor(urlEqualTo(CLOUD_EVENT_PATH)) - .withRequestBody(matchingJsonPath("$.id", equalTo(id))) - .withHeader("Content-Type", equalTo(APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8))); - } - - @Test - void executeWithInvalidCloudEvent() { - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/invalidCloudEvent") - .then() - .statusCode(HttpURLConnection.HTTP_INTERNAL_ERROR); - } - - @Test - void execute404() { - mockExecute404Endpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/serviceNotFound") - .then() - .statusCode(HttpURLConnection.HTTP_NOT_FOUND); - } - - @Test - void executeTimeout() { - mockExecuteTimeoutEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .body("{\"name\": \"hbelmiro\" }").when() - .post("/timeoutKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_INTERNAL_ERROR); - } - - @Test - void executeWithHeadersAsPlainJson() { - mockExecuteWithHeadersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/headersKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Headers added successfully")); - } - - @Test - void executeWithQueryParametersAsPlainJson() { - mockExecuteWithQueryParametersPostEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/queryParamsKnativeFunctionPost") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Query parameters added successfully")); - } - - @Test - void executeWithHeadersAndQueryParametersAsPlainJson() { - mockExecuteWithHeadersAndQueryParametersPostEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/headersAndQueryParamsKnativeFunctionPost") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Headers and query parameters added successfully")); - } - - @Test - void executeWithQueryParametersGet() { - mockExecuteWithQueryParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/queryParamsKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Query parameters added successfully")); - } - - @Test - void executeWithHeadersAndQueryParametersGet() { - mockExecuteWithHeadersAndQueryParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/headersAndQueryParamsKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Headers and query parameters added successfully")); - } - - @Test - void executeWithHeadersAsCloudEvent() { - mockExecuteWithHeadersCloudEventEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/cloudEventHeadersKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("CloudEvents with headers are awesome!")) - .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() - .put("long", 42L) - .put("String", "xpto").toPrettyString())); - } - - private void mockExecuteTimeoutEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/timeout")) - .willReturn(aResponse() - .withFixedDelay(requestTimeout.intValue() + 500) - .withStatus(200))); - } - - private void mockExecute404Endpoint() { - wireMockServer.stubFor(post(urlEqualTo("/non_existing_path")) - .willReturn(aResponse() - .withStatus(404))); - } - - private void mockExecuteCloudEventWithParametersEndpoint() { - wireMockServer.stubFor(post(urlEqualTo(CLOUD_EVENT_PATH)) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8) - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "CloudEvents are awesome!") - .put("object", JsonNodeFactory.instance.objectNode() - .put("long", 42L) - .put("String", "xpto").toPrettyString())))); - } - - private void mockExecuteWithEmptyParametersEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/emptyParamsKnativeFunction")) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("org", "Acme") - .put("project", "Kogito")))); - } - - private void mockExecuteWithParametersEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/plainJsonFunction")) - .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() - .put("name", "hbelmiro") - .toString())) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Hello")))); - } - - private void mockExecuteHttpGetEndpoint() { - wireMockServer.stubFor(get(urlEqualTo("/plainJsonFunction?name=hbelmiro")) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Hello")))); - } - - private void mockExecuteWithArrayEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/arrayFunction")) - .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() - .set("array", JsonNodeFactory.instance.arrayNode().add("Javierito").add("Pepito")) - .toString())) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", JsonNodeFactory.instance.arrayNode().add(23).add(24).toPrettyString())))); - } - - private void mockExecuteWithHeadersEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/headersFunction")) - .withHeader("Test", equalTo("test")) - .withHeader("Authorization", equalTo("Bearer token")) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Headers added successfully")))); - } - - private void mockExecuteWithQueryParametersPostEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/queryParamsFunction?param2=value2¶m1=value1")) - .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() - .put("param3", "value3") - .toString())) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Query parameters added successfully")))); - } - - private void mockExecuteWithHeadersAndQueryParametersPostEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/headersAndQueryParamsFunction?param1=value1")) - .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() - .put("param2", "value2") - .toString())) - .withHeader("Authorization", equalTo("Bearer token")) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Headers and query parameters added successfully")))); - } - - private void mockExecuteWithQueryParametersEndpoint() { - wireMockServer.stubFor(get(urlEqualTo("/queryParamsFunction?QUERY_param3=value3¶m1=value1¶m2=value2")) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Query parameters added successfully")))); - } - - private void mockExecuteWithHeadersAndQueryParametersEndpoint() { - wireMockServer.stubFor(get(urlEqualTo("/headersAndQueryParamsFunction?param1=value1")) - .withHeader("Authorization", equalTo("Bearer token")) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Headers and query parameters added successfully")))); - } - - private void mockExecuteWithHeadersCloudEventEndpoint() { - wireMockServer.stubFor(post(urlEqualTo(CLOUD_EVENT_PATH)) - .withHeader("Test", equalTo("test")) - .withHeader("Authorization", equalTo("Bearer token")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8) - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "CloudEvents with headers are awesome!") - .put("object", JsonNodeFactory.instance.objectNode() - .put("long", 42L) - .put("String", "xpto").toPrettyString())))); - } + public static final String AT_LEAST_ONE_NON_WHITE_CHARACTER_REGEX = ".*\\S.*"; + private static final String NAMESPACE = "default"; + private static final String SERVICENAME = "serverless-workflow-greeting-quarkus"; + private static final String CLOUD_EVENT_PATH = "/cloud-event"; + private static WireMockServer wireMockServer; + + private static String remoteServiceUrl; + + @ConfigProperty(name = "kogito.sw.functions.greet_with_timeout.timeout") + Long requestTimeout; + + @KubernetesTestServer + KubernetesServer mockServer; + + @BeforeAll + static void beforeAll() { + createWiremockServer(); + } + + @AfterAll + static void afterAll() { + if (wireMockServer != null) { + wireMockServer.stop(); + } + } + + private static void createWiremockServer() { + wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); + wireMockServer.start(); + remoteServiceUrl = wireMockServer.baseUrl(); + } + + @BeforeEach + void beforeEach() { + createKnativeServiceIfNotExists(mockServer.getClient(), "knative/quarkus-greeting.yaml", NAMESPACE, SERVICENAME, remoteServiceUrl); + } + + @Test + void executeHttpGet() { + mockExecuteHttpGetEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"name\": \"hbelmiro\" }").when() + .post("/getKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Hello")); + + wireMockServer.verify(getRequestedFor(urlEqualTo("/plainJsonFunction?name=hbelmiro"))); + } + + @Test + void executeWithEmptyParameters() { + mockExecuteWithEmptyParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"workflowdata\":{}}").when() + .post("/emptyParamsKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.org", is("Acme")) + .body("workflowdata.project", is("Kogito")); + } + + @Test + void executeWithParameters() { + mockExecuteWithParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"name\": \"hbelmiro\" }").when() + .post("/plainJsonKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Hello")); + } + + @Test + void executeWithArray() { + mockExecuteWithArrayEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/arrayKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is(JsonNodeFactory.instance.arrayNode().add(23).add(24).toPrettyString())); + } + + @Test + void executeWithParametersShouldSendOnlyFunctionArgs() { + mockExecuteWithParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"name\": \"hbelmiro\", \"should_not_be_sent\" : \"value\" }").when() + .post("/plainJsonKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Hello")); + } + + @Test + void executeWithCloudEventWithIdAsPlainJson() { + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/cloudEventWithIdAsPlainJson") + .then() + .statusCode(HttpURLConnection.HTTP_BAD_REQUEST); + } + + @Test + void executeWithCloudEventWithoutIdAsPlainJson() { + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/cloudEventWithoutIdAsPlainJson") + .then() + .statusCode(HttpURLConnection.HTTP_BAD_REQUEST); + } + + @Test + void executeCloudEvent() { + mockExecuteCloudEventWithParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .post("/cloudEvent") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("CloudEvents are awesome!")) + .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto").toPrettyString())); + + wireMockServer.verify(postRequestedFor(urlEqualTo(CLOUD_EVENT_PATH)) + .withRequestBody(matchingJsonPath("$.id", equalTo("42"))) + .withHeader("Content-Type", equalTo(APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8))); + } + + @Test + void executeCloudEventWithMissingIdShouldNotThrowException() { + mockExecuteCloudEventWithParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .post("/cloudEventWithMissingId") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("CloudEvents are awesome!")) + .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto").toPrettyString())); + + wireMockServer.verify(postRequestedFor(urlEqualTo(CLOUD_EVENT_PATH)) + .withRequestBody(matchingJsonPath("$.id", WireMock.matching(AT_LEAST_ONE_NON_WHITE_CHARACTER_REGEX))) + .withHeader("Content-Type", equalTo(APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8))); + } + + @Test + void cloudEventWithIdMustBeSentAsIs() { + mockExecuteCloudEventWithParametersEndpoint(); + + String id = "42"; + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"id\": \"" + id + "\" }").when() + .post("/cloudEventWithIdAsParam") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("CloudEvents are awesome!")) + .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto").toPrettyString())); + + wireMockServer.verify(postRequestedFor(urlEqualTo(CLOUD_EVENT_PATH)) + .withRequestBody(matchingJsonPath("$.id", equalTo(id))) + .withHeader("Content-Type", equalTo(APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8))); + } + + @Test + void executeWithInvalidCloudEvent() { + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/invalidCloudEvent") + .then() + .statusCode(HttpURLConnection.HTTP_INTERNAL_ERROR); + } + + @Test + void execute404() { + mockExecute404Endpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/serviceNotFound") + .then() + .statusCode(HttpURLConnection.HTTP_NOT_FOUND); + } + + @Test + void executeTimeout() { + mockExecuteTimeoutEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"name\": \"hbelmiro\" }").when() + .post("/timeoutKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_INTERNAL_ERROR); + } + + @Test + void executeWithHeadersAsPlainJson() { + mockExecuteWithHeadersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/headersKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Headers added successfully")); + } + + @Test + void executeWithQueryParametersAsPlainJson() { + mockExecuteWithQueryParametersPostEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/queryParamsKnativeFunctionPost") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Query parameters added successfully")); + } + + @Test + void executeWithHeadersAndQueryParametersAsPlainJson() { + mockExecuteWithHeadersAndQueryParametersPostEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/headersAndQueryParamsKnativeFunctionPost") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Headers and query parameters added successfully")); + } + + @Test + void executeWithQueryParametersGet() { + mockExecuteWithQueryParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/queryParamsKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Query parameters added successfully")); + } + + @Test + void executeWithHeadersAndQueryParametersGet() { + mockExecuteWithHeadersAndQueryParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/headersAndQueryParamsKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Headers and query parameters added successfully")); + } + + @Test + void executeWithHeadersAsCloudEvent() { + mockExecuteWithHeadersCloudEventEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/cloudEventHeadersKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("CloudEvents with headers are awesome!")) + .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto").toPrettyString())); + } + + private void mockExecuteTimeoutEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/timeout")) + .willReturn(aResponse() + .withFixedDelay(requestTimeout.intValue() + 500) + .withStatus(200))); + } + + private void mockExecute404Endpoint() { + wireMockServer.stubFor(post(urlEqualTo("/non_existing_path")) + .willReturn(aResponse() + .withStatus(404))); + } + + private void mockExecuteCloudEventWithParametersEndpoint() { + wireMockServer.stubFor(post(urlEqualTo(CLOUD_EVENT_PATH)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8) + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "CloudEvents are awesome!") + .put("object", JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto").toPrettyString())))); + } + + private void mockExecuteWithEmptyParametersEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/emptyParamsKnativeFunction")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("org", "Acme") + .put("project", "Kogito")))); + } + + private void mockExecuteWithParametersEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/plainJsonFunction")) + .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() + .put("name", "hbelmiro") + .toString())) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Hello")))); + } + + private void mockExecuteHttpGetEndpoint() { + wireMockServer.stubFor(get(urlEqualTo("/plainJsonFunction?name=hbelmiro")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Hello")))); + } + + private void mockExecuteWithArrayEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/arrayFunction")) + .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() + .set("array", JsonNodeFactory.instance.arrayNode().add("Javierito").add("Pepito")) + .toString())) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", JsonNodeFactory.instance.arrayNode().add(23).add(24).toPrettyString())))); + } + + private void mockExecuteWithHeadersEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/headersFunction")) + .withHeader("Test", equalTo("test")) + .withHeader("Authorization", equalTo("Bearer token")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Headers added successfully")))); + } + + private void mockExecuteWithQueryParametersPostEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/queryParamsFunction?param2=value2¶m1=value1")) + .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() + .put("param3", "value3") + .toString())) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Query parameters added successfully")))); + } + + private void mockExecuteWithHeadersAndQueryParametersPostEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/headersAndQueryParamsFunction?param1=value1")) + .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() + .put("param2", "value2") + .toString())) + .withHeader("Authorization", equalTo("Bearer token")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Headers and query parameters added successfully")))); + } + + private void mockExecuteWithQueryParametersEndpoint() { + wireMockServer.stubFor(get(urlEqualTo("/queryParamsFunction?QUERY_param3=value3¶m1=value1¶m2=value2")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Query parameters added successfully")))); + } + + private void mockExecuteWithHeadersAndQueryParametersEndpoint() { + wireMockServer.stubFor(get(urlEqualTo("/headersAndQueryParamsFunction?param1=value1")) + .withHeader("Authorization", equalTo("Bearer token")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Headers and query parameters added successfully")))); + } + + private void mockExecuteWithHeadersCloudEventEndpoint() { + wireMockServer.stubFor(post(urlEqualTo(CLOUD_EVENT_PATH)) + .withHeader("Test", equalTo("test")) + .withHeader("Authorization", equalTo("Bearer token")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8) + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "CloudEvents with headers are awesome!") + .put("object", JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto").toPrettyString())))); + } } From 8d90e4bdb1c3ac6521534ba348df35e114a6f310 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Thu, 29 May 2025 17:17:24 +0100 Subject: [PATCH 10/27] fixed OperationTests and format Signed-off-by: Pedro Escaleira --- .../PlainJsonKnativeParamsDecorator.java | 7 ++--- .../customfunctions/OperationTest.java | 28 +++++++++++++------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java index 56d34817a0a..bac9c7ffe35 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java @@ -19,7 +19,6 @@ package org.kie.kogito.addons.quarkus.knative.serving.customfunctions; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -32,15 +31,15 @@ import org.kogito.workitem.rest.RestWorkItemHandler; import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + import io.vertx.mutiny.ext.web.client.HttpRequest; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.CLOUDEVENT_SENT_AS_PLAIN_JSON_ERROR_MESSAGE; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.ID; import static org.kie.kogito.serverless.workflow.SWFConstants.MODEL_WORKFLOW_VAR; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - public final class PlainJsonKnativeParamsDecorator extends PrefixParamsDecorator { @Override diff --git a/quarkus/addons/knative/serving/runtime/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/OperationTest.java b/quarkus/addons/knative/serving/runtime/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/OperationTest.java index 020dd47315d..c5cfee9e2a3 100644 --- a/quarkus/addons/knative/serving/runtime/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/OperationTest.java +++ b/quarkus/addons/knative/serving/runtime/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/OperationTest.java @@ -29,8 +29,11 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.CLOUD_EVENT_PARAMETER_NAME; +import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.FAIL_ON_STATUS_ERROR_PARAMETER_NAME; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.METHOD_PARAMETER_NAME; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.PATH_PARAMETER_NAME; +import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.RETURN_HEADERS_PARAMETER_NAME; +import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.RETURN_STATUS_CODE_PARAMETER_NAME; class OperationTest { @@ -38,23 +41,32 @@ class OperationTest { public static Stream parseSource() { return Stream.of( - Arguments.of(SERVICE, Operation.builder().withService(SERVICE).build()), + Arguments.of(SERVICE, Operation.builder().withService(SERVICE).withFailOnStatusError(true).build()), - Arguments.of("service?", Operation.builder().withService(SERVICE).build()), + Arguments.of("service?", Operation.builder().withService(SERVICE).withFailOnStatusError(true).build()), - Arguments.of("service?" + PATH_PARAMETER_NAME + "=/my_path", Operation.builder().withService(SERVICE).withPath("/my_path").build()), + Arguments.of("service?" + PATH_PARAMETER_NAME + "=/my_path", Operation.builder().withService(SERVICE).withPath("/my_path").withFailOnStatusError(true).build()), - Arguments.of("service?" + CLOUD_EVENT_PARAMETER_NAME + "=true", Operation.builder().withService(SERVICE).withIsCloudEvent(true).build()), + Arguments.of("service?" + CLOUD_EVENT_PARAMETER_NAME + "=true", Operation.builder().withService(SERVICE).withFailOnStatusError(true).withIsCloudEvent(true).build()), - Arguments.of("service?" + METHOD_PARAMETER_NAME + "=GET", Operation.builder().withService(SERVICE).withMethod(HttpMethod.GET).build()), + Arguments.of("service?" + METHOD_PARAMETER_NAME + "=GET", Operation.builder().withService(SERVICE).withFailOnStatusError(true).withMethod(HttpMethod.GET).build()), - Arguments.of("service?" + METHOD_PARAMETER_NAME + "=get", Operation.builder().withService(SERVICE).withMethod(HttpMethod.GET).build()), + Arguments.of("service?" + METHOD_PARAMETER_NAME + "=get", Operation.builder().withService(SERVICE).withFailOnStatusError(true).withMethod(HttpMethod.GET).build()), Arguments.of("service?" + PATH_PARAMETER_NAME + "=/my_path&" + CLOUD_EVENT_PARAMETER_NAME + "=true", - Operation.builder().withService(SERVICE).withPath("/my_path").withIsCloudEvent(true).build()), + Operation.builder().withService(SERVICE).withPath("/my_path").withFailOnStatusError(true).withIsCloudEvent(true).build()), Arguments.of("service?" + PATH_PARAMETER_NAME + "=/my_path&" + CLOUD_EVENT_PARAMETER_NAME + "=false&" + METHOD_PARAMETER_NAME + "=GET", - Operation.builder().withService(SERVICE).withPath("/my_path").withIsCloudEvent(false).withMethod(HttpMethod.GET).build())); + Operation.builder().withService(SERVICE).withPath("/my_path").withFailOnStatusError(true).withIsCloudEvent(false).withMethod(HttpMethod.GET).build()), + + Arguments.of("service?" + FAIL_ON_STATUS_ERROR_PARAMETER_NAME + "=false", + Operation.builder().withService(SERVICE).withFailOnStatusError(false).build()), + + Arguments.of("service?" + RETURN_HEADERS_PARAMETER_NAME + "=true", + Operation.builder().withService(SERVICE).withFailOnStatusError(true).withReturnHeaders(true).build()), + + Arguments.of("service?" + RETURN_STATUS_CODE_PARAMETER_NAME + "=true", + Operation.builder().withService(SERVICE).withFailOnStatusError(true).withReturnStatusCode(true).build())); } public static Stream invalidOperationSource() { From 033521a8162f7858fbf8d93dddcc1028a6cd41a8 Mon Sep 17 00:00:00 2001 From: escaleira Date: Fri, 30 May 2025 21:57:37 +0100 Subject: [PATCH 11/27] small fix for the tests Signed-off-by: escaleira --- ...adersAndQueryParamsKnativeFunction.sw.json | 2 +- .../it/KnativeServingAddonIT.java | 24 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/quarkus/addons/knative/serving/integration-tests/src/main/resources/headersAndQueryParamsKnativeFunction.sw.json b/quarkus/addons/knative/serving/integration-tests/src/main/resources/headersAndQueryParamsKnativeFunction.sw.json index c8b1fc99f23..5d8d5c99d2f 100644 --- a/quarkus/addons/knative/serving/integration-tests/src/main/resources/headersAndQueryParamsKnativeFunction.sw.json +++ b/quarkus/addons/knative/serving/integration-tests/src/main/resources/headersAndQueryParamsKnativeFunction.sw.json @@ -20,7 +20,7 @@ "functionRef": { "refName": "greet", "arguments": { - "QUERY_param1": "value1", + "param1": "value1", "HEADER_Authorization": "Bearer token" } } diff --git a/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java b/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java index 1328f026012..d7bea95ba84 100644 --- a/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java +++ b/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java @@ -89,7 +89,8 @@ private static void createWiremockServer() { @BeforeEach void beforeEach() { - createKnativeServiceIfNotExists(mockServer.getClient(), "knative/quarkus-greeting.yaml", NAMESPACE, SERVICENAME, remoteServiceUrl); + createKnativeServiceIfNotExists(mockServer.getClient(), "knative/quarkus-greeting.yaml", NAMESPACE, + SERVICENAME, remoteServiceUrl); } @Test @@ -147,7 +148,8 @@ void executeWithArray() { .post("/arrayKnativeFunction") .then() .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is(JsonNodeFactory.instance.arrayNode().add(23).add(24).toPrettyString())); + .body("workflowdata.message", is( + JsonNodeFactory.instance.arrayNode().add(23).add(24).toPrettyString())); } @Test @@ -220,7 +222,8 @@ void executeCloudEventWithMissingIdShouldNotThrowException() { .put("String", "xpto").toPrettyString())); wireMockServer.verify(postRequestedFor(urlEqualTo(CLOUD_EVENT_PATH)) - .withRequestBody(matchingJsonPath("$.id", WireMock.matching(AT_LEAST_ONE_NON_WHITE_CHARACTER_REGEX))) + .withRequestBody(matchingJsonPath("$.id", + WireMock.matching(AT_LEAST_ONE_NON_WHITE_CHARACTER_REGEX))) .withHeader("Content-Type", equalTo(APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8))); } @@ -385,7 +388,8 @@ private void mockExecuteCloudEventWithParametersEndpoint() { .put("message", "CloudEvents are awesome!") .put("object", JsonNodeFactory.instance.objectNode() .put("long", 42L) - .put("String", "xpto").toPrettyString())))); + .put("String", "xpto") + .toPrettyString())))); } private void mockExecuteWithEmptyParametersEndpoint() { @@ -422,13 +426,15 @@ private void mockExecuteHttpGetEndpoint() { private void mockExecuteWithArrayEndpoint() { wireMockServer.stubFor(post(urlEqualTo("/arrayFunction")) .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() - .set("array", JsonNodeFactory.instance.arrayNode().add("Javierito").add("Pepito")) + .set("array", JsonNodeFactory.instance.arrayNode().add("Javierito") + .add("Pepito")) .toString())) .willReturn(aResponse() .withStatus(HttpURLConnection.HTTP_OK) .withHeader("Content-Type", "application/json") .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", JsonNodeFactory.instance.arrayNode().add(23).add(24).toPrettyString())))); + .put("message", JsonNodeFactory.instance.arrayNode() + .add(23).add(24).toPrettyString())))); } private void mockExecuteWithHeadersEndpoint() { @@ -468,7 +474,8 @@ private void mockExecuteWithHeadersAndQueryParametersPostEndpoint() { } private void mockExecuteWithQueryParametersEndpoint() { - wireMockServer.stubFor(get(urlEqualTo("/queryParamsFunction?QUERY_param3=value3¶m1=value1¶m2=value2")) + wireMockServer.stubFor(get( + urlEqualTo("/queryParamsFunction?QUERY_param3=value3¶m1=value1¶m2=value2")) .willReturn(aResponse() .withStatus(HttpURLConnection.HTTP_OK) .withHeader("Content-Type", "application/json") @@ -497,6 +504,7 @@ private void mockExecuteWithHeadersCloudEventEndpoint() { .put("message", "CloudEvents with headers are awesome!") .put("object", JsonNodeFactory.instance.objectNode() .put("long", 42L) - .put("String", "xpto").toPrettyString())))); + .put("String", "xpto") + .toPrettyString())))); } } From db2e0e7d71d1009ef70dbf869372768f2cd40c55 Mon Sep 17 00:00:00 2001 From: escaleira Date: Sat, 31 May 2025 00:35:30 +0100 Subject: [PATCH 12/27] formatting Signed-off-by: escaleira --- .../it/KnativeServingAddonIT.java | 904 +++++++++--------- 1 file changed, 452 insertions(+), 452 deletions(-) diff --git a/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java b/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java index d7bea95ba84..d908ec863d4 100644 --- a/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java +++ b/quarkus/addons/knative/serving/integration-tests/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/it/KnativeServingAddonIT.java @@ -55,456 +55,456 @@ @WithKubernetesTestServer class KnativeServingAddonIT { - public static final String AT_LEAST_ONE_NON_WHITE_CHARACTER_REGEX = ".*\\S.*"; - private static final String NAMESPACE = "default"; - private static final String SERVICENAME = "serverless-workflow-greeting-quarkus"; - private static final String CLOUD_EVENT_PATH = "/cloud-event"; - private static WireMockServer wireMockServer; - - private static String remoteServiceUrl; - - @ConfigProperty(name = "kogito.sw.functions.greet_with_timeout.timeout") - Long requestTimeout; - - @KubernetesTestServer - KubernetesServer mockServer; - - @BeforeAll - static void beforeAll() { - createWiremockServer(); - } - - @AfterAll - static void afterAll() { - if (wireMockServer != null) { - wireMockServer.stop(); - } - } - - private static void createWiremockServer() { - wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); - wireMockServer.start(); - remoteServiceUrl = wireMockServer.baseUrl(); - } - - @BeforeEach - void beforeEach() { - createKnativeServiceIfNotExists(mockServer.getClient(), "knative/quarkus-greeting.yaml", NAMESPACE, - SERVICENAME, remoteServiceUrl); - } - - @Test - void executeHttpGet() { - mockExecuteHttpGetEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .body("{\"name\": \"hbelmiro\" }").when() - .post("/getKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Hello")); - - wireMockServer.verify(getRequestedFor(urlEqualTo("/plainJsonFunction?name=hbelmiro"))); - } - - @Test - void executeWithEmptyParameters() { - mockExecuteWithEmptyParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .body("{\"workflowdata\":{}}").when() - .post("/emptyParamsKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.org", is("Acme")) - .body("workflowdata.project", is("Kogito")); - } - - @Test - void executeWithParameters() { - mockExecuteWithParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .body("{\"name\": \"hbelmiro\" }").when() - .post("/plainJsonKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Hello")); - } - - @Test - void executeWithArray() { - mockExecuteWithArrayEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/arrayKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is( - JsonNodeFactory.instance.arrayNode().add(23).add(24).toPrettyString())); - } - - @Test - void executeWithParametersShouldSendOnlyFunctionArgs() { - mockExecuteWithParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .body("{\"name\": \"hbelmiro\", \"should_not_be_sent\" : \"value\" }").when() - .post("/plainJsonKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Hello")); - } - - @Test - void executeWithCloudEventWithIdAsPlainJson() { - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/cloudEventWithIdAsPlainJson") - .then() - .statusCode(HttpURLConnection.HTTP_BAD_REQUEST); - } - - @Test - void executeWithCloudEventWithoutIdAsPlainJson() { - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/cloudEventWithoutIdAsPlainJson") - .then() - .statusCode(HttpURLConnection.HTTP_BAD_REQUEST); - } - - @Test - void executeCloudEvent() { - mockExecuteCloudEventWithParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .post("/cloudEvent") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("CloudEvents are awesome!")) - .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() - .put("long", 42L) - .put("String", "xpto").toPrettyString())); - - wireMockServer.verify(postRequestedFor(urlEqualTo(CLOUD_EVENT_PATH)) - .withRequestBody(matchingJsonPath("$.id", equalTo("42"))) - .withHeader("Content-Type", equalTo(APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8))); - } - - @Test - void executeCloudEventWithMissingIdShouldNotThrowException() { - mockExecuteCloudEventWithParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .post("/cloudEventWithMissingId") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("CloudEvents are awesome!")) - .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() - .put("long", 42L) - .put("String", "xpto").toPrettyString())); - - wireMockServer.verify(postRequestedFor(urlEqualTo(CLOUD_EVENT_PATH)) - .withRequestBody(matchingJsonPath("$.id", - WireMock.matching(AT_LEAST_ONE_NON_WHITE_CHARACTER_REGEX))) - .withHeader("Content-Type", equalTo(APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8))); - } - - @Test - void cloudEventWithIdMustBeSentAsIs() { - mockExecuteCloudEventWithParametersEndpoint(); - - String id = "42"; - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .body("{\"id\": \"" + id + "\" }").when() - .post("/cloudEventWithIdAsParam") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("CloudEvents are awesome!")) - .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() - .put("long", 42L) - .put("String", "xpto").toPrettyString())); - - wireMockServer.verify(postRequestedFor(urlEqualTo(CLOUD_EVENT_PATH)) - .withRequestBody(matchingJsonPath("$.id", equalTo(id))) - .withHeader("Content-Type", equalTo(APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8))); - } - - @Test - void executeWithInvalidCloudEvent() { - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/invalidCloudEvent") - .then() - .statusCode(HttpURLConnection.HTTP_INTERNAL_ERROR); - } - - @Test - void execute404() { - mockExecute404Endpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/serviceNotFound") - .then() - .statusCode(HttpURLConnection.HTTP_NOT_FOUND); - } - - @Test - void executeTimeout() { - mockExecuteTimeoutEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .body("{\"name\": \"hbelmiro\" }").when() - .post("/timeoutKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_INTERNAL_ERROR); - } - - @Test - void executeWithHeadersAsPlainJson() { - mockExecuteWithHeadersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/headersKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Headers added successfully")); - } - - @Test - void executeWithQueryParametersAsPlainJson() { - mockExecuteWithQueryParametersPostEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/queryParamsKnativeFunctionPost") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Query parameters added successfully")); - } - - @Test - void executeWithHeadersAndQueryParametersAsPlainJson() { - mockExecuteWithHeadersAndQueryParametersPostEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/headersAndQueryParamsKnativeFunctionPost") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Headers and query parameters added successfully")); - } - - @Test - void executeWithQueryParametersGet() { - mockExecuteWithQueryParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/queryParamsKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Query parameters added successfully")); - } - - @Test - void executeWithHeadersAndQueryParametersGet() { - mockExecuteWithHeadersAndQueryParametersEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/headersAndQueryParamsKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("Headers and query parameters added successfully")); - } - - @Test - void executeWithHeadersAsCloudEvent() { - mockExecuteWithHeadersCloudEventEndpoint(); - - given() - .contentType(ContentType.JSON) - .accept(ContentType.JSON).when() - .post("/cloudEventHeadersKnativeFunction") - .then() - .statusCode(HttpURLConnection.HTTP_CREATED) - .body("workflowdata.message", is("CloudEvents with headers are awesome!")) - .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() - .put("long", 42L) - .put("String", "xpto").toPrettyString())); - } - - private void mockExecuteTimeoutEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/timeout")) - .willReturn(aResponse() - .withFixedDelay(requestTimeout.intValue() + 500) - .withStatus(200))); - } - - private void mockExecute404Endpoint() { - wireMockServer.stubFor(post(urlEqualTo("/non_existing_path")) - .willReturn(aResponse() - .withStatus(404))); - } - - private void mockExecuteCloudEventWithParametersEndpoint() { - wireMockServer.stubFor(post(urlEqualTo(CLOUD_EVENT_PATH)) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8) - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "CloudEvents are awesome!") - .put("object", JsonNodeFactory.instance.objectNode() - .put("long", 42L) - .put("String", "xpto") - .toPrettyString())))); - } - - private void mockExecuteWithEmptyParametersEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/emptyParamsKnativeFunction")) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("org", "Acme") - .put("project", "Kogito")))); - } - - private void mockExecuteWithParametersEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/plainJsonFunction")) - .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() - .put("name", "hbelmiro") - .toString())) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Hello")))); - } - - private void mockExecuteHttpGetEndpoint() { - wireMockServer.stubFor(get(urlEqualTo("/plainJsonFunction?name=hbelmiro")) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Hello")))); - } - - private void mockExecuteWithArrayEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/arrayFunction")) - .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() - .set("array", JsonNodeFactory.instance.arrayNode().add("Javierito") - .add("Pepito")) - .toString())) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", JsonNodeFactory.instance.arrayNode() - .add(23).add(24).toPrettyString())))); - } - - private void mockExecuteWithHeadersEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/headersFunction")) - .withHeader("Test", equalTo("test")) - .withHeader("Authorization", equalTo("Bearer token")) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Headers added successfully")))); - } - - private void mockExecuteWithQueryParametersPostEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/queryParamsFunction?param2=value2¶m1=value1")) - .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() - .put("param3", "value3") - .toString())) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Query parameters added successfully")))); - } - - private void mockExecuteWithHeadersAndQueryParametersPostEndpoint() { - wireMockServer.stubFor(post(urlEqualTo("/headersAndQueryParamsFunction?param1=value1")) - .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() - .put("param2", "value2") - .toString())) - .withHeader("Authorization", equalTo("Bearer token")) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Headers and query parameters added successfully")))); - } - - private void mockExecuteWithQueryParametersEndpoint() { - wireMockServer.stubFor(get( - urlEqualTo("/queryParamsFunction?QUERY_param3=value3¶m1=value1¶m2=value2")) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Query parameters added successfully")))); - } - - private void mockExecuteWithHeadersAndQueryParametersEndpoint() { - wireMockServer.stubFor(get(urlEqualTo("/headersAndQueryParamsFunction?param1=value1")) - .withHeader("Authorization", equalTo("Bearer token")) - .willReturn(aResponse() - .withStatus(HttpURLConnection.HTTP_OK) - .withHeader("Content-Type", "application/json") - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "Headers and query parameters added successfully")))); - } - - private void mockExecuteWithHeadersCloudEventEndpoint() { - wireMockServer.stubFor(post(urlEqualTo(CLOUD_EVENT_PATH)) - .withHeader("Test", equalTo("test")) - .withHeader("Authorization", equalTo("Bearer token")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8) - .withJsonBody(JsonNodeFactory.instance.objectNode() - .put("message", "CloudEvents with headers are awesome!") - .put("object", JsonNodeFactory.instance.objectNode() - .put("long", 42L) - .put("String", "xpto") - .toPrettyString())))); - } + public static final String AT_LEAST_ONE_NON_WHITE_CHARACTER_REGEX = ".*\\S.*"; + private static final String NAMESPACE = "default"; + private static final String SERVICENAME = "serverless-workflow-greeting-quarkus"; + private static final String CLOUD_EVENT_PATH = "/cloud-event"; + private static WireMockServer wireMockServer; + + private static String remoteServiceUrl; + + @ConfigProperty(name = "kogito.sw.functions.greet_with_timeout.timeout") + Long requestTimeout; + + @KubernetesTestServer + KubernetesServer mockServer; + + @BeforeAll + static void beforeAll() { + createWiremockServer(); + } + + @AfterAll + static void afterAll() { + if (wireMockServer != null) { + wireMockServer.stop(); + } + } + + private static void createWiremockServer() { + wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); + wireMockServer.start(); + remoteServiceUrl = wireMockServer.baseUrl(); + } + + @BeforeEach + void beforeEach() { + createKnativeServiceIfNotExists(mockServer.getClient(), "knative/quarkus-greeting.yaml", NAMESPACE, + SERVICENAME, remoteServiceUrl); + } + + @Test + void executeHttpGet() { + mockExecuteHttpGetEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"name\": \"hbelmiro\" }").when() + .post("/getKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Hello")); + + wireMockServer.verify(getRequestedFor(urlEqualTo("/plainJsonFunction?name=hbelmiro"))); + } + + @Test + void executeWithEmptyParameters() { + mockExecuteWithEmptyParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"workflowdata\":{}}").when() + .post("/emptyParamsKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.org", is("Acme")) + .body("workflowdata.project", is("Kogito")); + } + + @Test + void executeWithParameters() { + mockExecuteWithParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"name\": \"hbelmiro\" }").when() + .post("/plainJsonKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Hello")); + } + + @Test + void executeWithArray() { + mockExecuteWithArrayEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/arrayKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is( + JsonNodeFactory.instance.arrayNode().add(23).add(24).toPrettyString())); + } + + @Test + void executeWithParametersShouldSendOnlyFunctionArgs() { + mockExecuteWithParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"name\": \"hbelmiro\", \"should_not_be_sent\" : \"value\" }").when() + .post("/plainJsonKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Hello")); + } + + @Test + void executeWithCloudEventWithIdAsPlainJson() { + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/cloudEventWithIdAsPlainJson") + .then() + .statusCode(HttpURLConnection.HTTP_BAD_REQUEST); + } + + @Test + void executeWithCloudEventWithoutIdAsPlainJson() { + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/cloudEventWithoutIdAsPlainJson") + .then() + .statusCode(HttpURLConnection.HTTP_BAD_REQUEST); + } + + @Test + void executeCloudEvent() { + mockExecuteCloudEventWithParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .post("/cloudEvent") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("CloudEvents are awesome!")) + .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto").toPrettyString())); + + wireMockServer.verify(postRequestedFor(urlEqualTo(CLOUD_EVENT_PATH)) + .withRequestBody(matchingJsonPath("$.id", equalTo("42"))) + .withHeader("Content-Type", equalTo(APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8))); + } + + @Test + void executeCloudEventWithMissingIdShouldNotThrowException() { + mockExecuteCloudEventWithParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .post("/cloudEventWithMissingId") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("CloudEvents are awesome!")) + .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto").toPrettyString())); + + wireMockServer.verify(postRequestedFor(urlEqualTo(CLOUD_EVENT_PATH)) + .withRequestBody(matchingJsonPath("$.id", + WireMock.matching(AT_LEAST_ONE_NON_WHITE_CHARACTER_REGEX))) + .withHeader("Content-Type", equalTo(APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8))); + } + + @Test + void cloudEventWithIdMustBeSentAsIs() { + mockExecuteCloudEventWithParametersEndpoint(); + + String id = "42"; + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"id\": \"" + id + "\" }").when() + .post("/cloudEventWithIdAsParam") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("CloudEvents are awesome!")) + .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto").toPrettyString())); + + wireMockServer.verify(postRequestedFor(urlEqualTo(CLOUD_EVENT_PATH)) + .withRequestBody(matchingJsonPath("$.id", equalTo(id))) + .withHeader("Content-Type", equalTo(APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8))); + } + + @Test + void executeWithInvalidCloudEvent() { + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/invalidCloudEvent") + .then() + .statusCode(HttpURLConnection.HTTP_INTERNAL_ERROR); + } + + @Test + void execute404() { + mockExecute404Endpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/serviceNotFound") + .then() + .statusCode(HttpURLConnection.HTTP_NOT_FOUND); + } + + @Test + void executeTimeout() { + mockExecuteTimeoutEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .body("{\"name\": \"hbelmiro\" }").when() + .post("/timeoutKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_INTERNAL_ERROR); + } + + @Test + void executeWithHeadersAsPlainJson() { + mockExecuteWithHeadersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/headersKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Headers added successfully")); + } + + @Test + void executeWithQueryParametersAsPlainJson() { + mockExecuteWithQueryParametersPostEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/queryParamsKnativeFunctionPost") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Query parameters added successfully")); + } + + @Test + void executeWithHeadersAndQueryParametersAsPlainJson() { + mockExecuteWithHeadersAndQueryParametersPostEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/headersAndQueryParamsKnativeFunctionPost") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Headers and query parameters added successfully")); + } + + @Test + void executeWithQueryParametersGet() { + mockExecuteWithQueryParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/queryParamsKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Query parameters added successfully")); + } + + @Test + void executeWithHeadersAndQueryParametersGet() { + mockExecuteWithHeadersAndQueryParametersEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/headersAndQueryParamsKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("Headers and query parameters added successfully")); + } + + @Test + void executeWithHeadersAsCloudEvent() { + mockExecuteWithHeadersCloudEventEndpoint(); + + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON).when() + .post("/cloudEventHeadersKnativeFunction") + .then() + .statusCode(HttpURLConnection.HTTP_CREATED) + .body("workflowdata.message", is("CloudEvents with headers are awesome!")) + .body("workflowdata.object", is(JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto").toPrettyString())); + } + + private void mockExecuteTimeoutEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/timeout")) + .willReturn(aResponse() + .withFixedDelay(requestTimeout.intValue() + 500) + .withStatus(200))); + } + + private void mockExecute404Endpoint() { + wireMockServer.stubFor(post(urlEqualTo("/non_existing_path")) + .willReturn(aResponse() + .withStatus(404))); + } + + private void mockExecuteCloudEventWithParametersEndpoint() { + wireMockServer.stubFor(post(urlEqualTo(CLOUD_EVENT_PATH)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8) + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "CloudEvents are awesome!") + .put("object", JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto") + .toPrettyString())))); + } + + private void mockExecuteWithEmptyParametersEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/emptyParamsKnativeFunction")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("org", "Acme") + .put("project", "Kogito")))); + } + + private void mockExecuteWithParametersEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/plainJsonFunction")) + .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() + .put("name", "hbelmiro") + .toString())) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Hello")))); + } + + private void mockExecuteHttpGetEndpoint() { + wireMockServer.stubFor(get(urlEqualTo("/plainJsonFunction?name=hbelmiro")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Hello")))); + } + + private void mockExecuteWithArrayEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/arrayFunction")) + .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() + .set("array", JsonNodeFactory.instance.arrayNode().add("Javierito") + .add("Pepito")) + .toString())) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", JsonNodeFactory.instance.arrayNode() + .add(23).add(24).toPrettyString())))); + } + + private void mockExecuteWithHeadersEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/headersFunction")) + .withHeader("Test", equalTo("test")) + .withHeader("Authorization", equalTo("Bearer token")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Headers added successfully")))); + } + + private void mockExecuteWithQueryParametersPostEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/queryParamsFunction?param2=value2¶m1=value1")) + .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() + .put("param3", "value3") + .toString())) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Query parameters added successfully")))); + } + + private void mockExecuteWithHeadersAndQueryParametersPostEndpoint() { + wireMockServer.stubFor(post(urlEqualTo("/headersAndQueryParamsFunction?param1=value1")) + .withRequestBody(equalToJson(JsonNodeFactory.instance.objectNode() + .put("param2", "value2") + .toString())) + .withHeader("Authorization", equalTo("Bearer token")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Headers and query parameters added successfully")))); + } + + private void mockExecuteWithQueryParametersEndpoint() { + wireMockServer.stubFor(get( + urlEqualTo("/queryParamsFunction?QUERY_param3=value3¶m1=value1¶m2=value2")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Query parameters added successfully")))); + } + + private void mockExecuteWithHeadersAndQueryParametersEndpoint() { + wireMockServer.stubFor(get(urlEqualTo("/headersAndQueryParamsFunction?param1=value1")) + .withHeader("Authorization", equalTo("Bearer token")) + .willReturn(aResponse() + .withStatus(HttpURLConnection.HTTP_OK) + .withHeader("Content-Type", "application/json") + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "Headers and query parameters added successfully")))); + } + + private void mockExecuteWithHeadersCloudEventEndpoint() { + wireMockServer.stubFor(post(urlEqualTo(CLOUD_EVENT_PATH)) + .withHeader("Test", equalTo("test")) + .withHeader("Authorization", equalTo("Bearer token")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", APPLICATION_CLOUDEVENTS_JSON_CHARSET_UTF_8) + .withJsonBody(JsonNodeFactory.instance.objectNode() + .put("message", "CloudEvents with headers are awesome!") + .put("object", JsonNodeFactory.instance.objectNode() + .put("long", 42L) + .put("String", "xpto") + .toPrettyString())))); + } } From d75081a75b1348384923cb5c2d8101aca4be6347 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Tue, 3 Jun 2025 17:07:48 +0100 Subject: [PATCH 13/27] small fix Signed-off-by: Pedro Escaleira --- .../PlainJsonKnativeParamsDecorator.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java index bac9c7ffe35..085a7de47dd 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java @@ -27,12 +27,12 @@ import org.kie.kogito.event.cloudevents.utils.CloudEventUtils; import org.kie.kogito.internal.process.workitem.KogitoWorkItem; -import org.kie.kogito.jackson.utils.ObjectNodeListenerAware; import org.kogito.workitem.rest.RestWorkItemHandler; import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.vertx.mutiny.ext.web.client.HttpRequest; @@ -59,9 +59,10 @@ private void buildFromParams(KogitoWorkItem workItem, Map parame Map inputModel = new HashMap<>(); Object inputModelObject = parameters.get(MODEL_WORKFLOW_VAR); - if (inputModelObject != null && inputModelObject instanceof ObjectNodeListenerAware) { + + if (inputModelObject != null && inputModelObject instanceof ObjectNode) { ObjectMapper mapper = new ObjectMapper(); - ((ObjectNodeListenerAware) inputModelObject).fields().forEachRemaining(entry -> { + ((ObjectNode) inputModelObject).fields().forEachRemaining(entry -> { JsonNode value = entry.getValue(); Object rawValue = mapper.convertValue(value, Object.class); inputModel.put(entry.getKey(), rawValue); @@ -75,8 +76,8 @@ private void buildFromParams(KogitoWorkItem workItem, Map parame if (filteredParams.isEmpty()) { Set paramsRemove = super.extractHeadersQueries(workItem, inputModel, request); - if (inputModelObject != null && inputModelObject instanceof ObjectNodeListenerAware) { - ((ObjectNodeListenerAware) inputModelObject).remove(paramsRemove); + if (inputModelObject != null && inputModelObject instanceof ObjectNode) { + ((ObjectNode) inputModelObject).remove(paramsRemove); } } else { super.decorate(workItem, parameters, request); From ec21e7bbef3df5cb818612737cb6bd694fa2ff0e Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Tue, 9 Sep 2025 17:55:30 +0100 Subject: [PATCH 14/27] fix extractJsonNodeValue Signed-off-by: Pedro Escaleira --- .../DefaultRestWorkItemHandlerResult.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java index 9c1ef61cd38..5edaa137c0c 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java @@ -20,6 +20,9 @@ import java.util.HashMap; import java.util.Map; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; @@ -88,6 +91,23 @@ private static Object extractJsonNodeValue(JsonNode node) { return node.booleanValue(); if (node.isNull()) return null; + if (node.isArray()) { + // Wrap the Iterator in a Spliterator and create a Stream + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize(node.elements(), 0), + false + ) + .map(DefaultRestWorkItemHandlerResult::extractJsonNodeValue) + .collect(Collectors.toList()); + } + if (node.isObject()) { + // Handle objects by recursively processing each field + Map result = new HashMap<>(); + node.fields().forEachRemaining(entry -> + result.put(entry.getKey(), extractJsonNodeValue(entry.getValue())) + ); + return result; + } return node.toString(); } } From a0374d9f5e333815d5674f416b906ca6c5b3ae0d Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Fri, 12 Sep 2025 13:23:55 +0100 Subject: [PATCH 15/27] small fix because of states that share the same execution scope (e.g. branches of parallel states) Signed-off-by: Pedro Escaleira --- .../PlainJsonKnativeParamsDecorator.java | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java index 085a7de47dd..1d6b5385cba 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java @@ -21,15 +21,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; -import java.util.stream.Collectors; import org.kie.kogito.event.cloudevents.utils.CloudEventUtils; import org.kie.kogito.internal.process.workitem.KogitoWorkItem; -import org.kogito.workitem.rest.RestWorkItemHandler; import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -60,27 +58,31 @@ private void buildFromParams(KogitoWorkItem workItem, Map parame Object inputModelObject = parameters.get(MODEL_WORKFLOW_VAR); - if (inputModelObject != null && inputModelObject instanceof ObjectNode) { + ObjectNode inputModelCopy = null; + if (inputModelObject instanceof ObjectNode objectNode) { ObjectMapper mapper = new ObjectMapper(); - ((ObjectNode) inputModelObject).fields().forEachRemaining(entry -> { + objectNode.fields().forEachRemaining(entry -> { JsonNode value = entry.getValue(); Object rawValue = mapper.convertValue(value, Object.class); inputModel.put(entry.getKey(), rawValue); }); + + try { + inputModelCopy = (ObjectNode) mapper.readTree(objectNode.toString()); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to copy MODEL_WORKFLOW_VAR", e); + } } - Set keysFilter = Set.of(RestWorkItemHandler.REQUEST_TIMEOUT_IN_MILLIS, MODEL_WORKFLOW_VAR); - Map filteredParams = parameters.entrySet().stream() - .filter(entry -> !keysFilter.contains(entry.getKey())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + Set paramsRemove = super.extractHeadersQueries(workItem, inputModel, request); + super.decorate(workItem, parameters, request); - if (filteredParams.isEmpty()) { - Set paramsRemove = super.extractHeadersQueries(workItem, inputModel, request); - if (inputModelObject != null && inputModelObject instanceof ObjectNode) { - ((ObjectNode) inputModelObject).remove(paramsRemove); - } - } else { - super.decorate(workItem, parameters, request); + if (inputModelCopy != null) { + // mutate the safe copy + inputModelCopy.remove(paramsRemove); + + // replace the original entry in parameters with the copy + parameters.put(MODEL_WORKFLOW_VAR, inputModelCopy); } } } From 3ded6c66e139734a4d9bd7e17c55907d7e96a6d1 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Mon, 15 Sep 2025 13:18:10 +0100 Subject: [PATCH 16/27] formatting issues Signed-off-by: Pedro Escaleira --- .../DefaultRestWorkItemHandlerResult.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java index 27cd8f9bdb3..bbea7adce1d 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java @@ -93,18 +93,15 @@ private static Object extractJsonNodeValue(JsonNode node) { if (node.isArray()) { // Wrap the Iterator in a Spliterator and create a Stream return StreamSupport.stream( - Spliterators.spliteratorUnknownSize(node.elements(), 0), - false - ) - .map(DefaultRestWorkItemHandlerResult::extractJsonNodeValue) - .collect(Collectors.toList()); + Spliterators.spliteratorUnknownSize(node.elements(), 0), + false) + .map(DefaultRestWorkItemHandlerResult::extractJsonNodeValue) + .collect(Collectors.toList()); } if (node.isObject()) { // Handle objects by recursively processing each field Map result = new HashMap<>(); - node.fields().forEachRemaining(entry -> - result.put(entry.getKey(), extractJsonNodeValue(entry.getValue())) - ); + node.fields().forEachRemaining(entry -> result.put(entry.getKey(), extractJsonNodeValue(entry.getValue()))); return result; } return node.toString(); From ad6c6595330cd4eb1150fb2f136a4f51659b928f Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Mon, 15 Sep 2025 16:32:48 +0100 Subject: [PATCH 17/27] fixed checkStatusCode() call to still work with .statusCode Signed-off-by: Pedro Escaleira --- .../org/kogito/workitem/rest/RestWorkItemHandler.java | 6 +----- .../DefaultRestWorkItemHandlerResult.java | 10 +++++++++- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java index c50f78d3b83..f7ea2d2ca44 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java @@ -142,7 +142,7 @@ public Optional activateWorkItemHandler(KogitoWorkItemManage boolean returnHeaders = getParam(parameters, RETURN_HEADERS, Boolean.class, false); boolean returnStatusCode = getParam(parameters, RETURN_STATUS_CODE, Boolean.class, false); - DEFAULT_RESULT_HANDLER = new DefaultRestWorkItemHandlerResult(returnHeaders, returnStatusCode); + DEFAULT_RESULT_HANDLER = new DefaultRestWorkItemHandlerResult(returnHeaders, returnStatusCode, failOnStatusError); HttpMethod method = getParam(parameters, METHOD, HttpMethod.class, HttpMethod.GET); RestWorkItemHandlerResult resultHandler = getClassParam(parameters, RESULT_HANDLER, RestWorkItemHandlerResult.class, DEFAULT_RESULT_HANDLER, resultHandlers); @@ -203,10 +203,6 @@ public Optional activateWorkItemHandler(KogitoWorkItemManage ? sendBody(request, bodyBuilder.apply(parameters), requestTimeout) : send(request, requestTimeout); - if (failOnStatusError) { - checkStatusCode(response); - } - return Optional.of(this.workItemLifeCycle.newTransition("complete", workItem.getPhaseStatus(), Collections.singletonMap(RESULT, resultHandler.apply(response, targetInfo, ContextFactory.fromItem(workItem))))); } diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java index bbea7adce1d..f4946a85a25 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java @@ -18,6 +18,8 @@ */ package org.kogito.workitem.rest.resulthandlers; +import static org.kogito.workitem.rest.RestWorkItemHandlerUtils.checkStatusCode; + import java.util.HashMap; import java.util.Map; import java.util.Spliterators; @@ -38,14 +40,20 @@ public class DefaultRestWorkItemHandlerResult implements RestWorkItemHandlerResu private boolean returnHeaders = false; private boolean returnStatusCode = false; + private boolean failOnStatusError = true; - public DefaultRestWorkItemHandlerResult(boolean returnHeaders, boolean returnStatusCode) { + public DefaultRestWorkItemHandlerResult(boolean returnHeaders, boolean returnStatusCode, boolean failOnStatusError) { this.returnHeaders = returnHeaders; this.returnStatusCode = returnStatusCode; + this.failOnStatusError = failOnStatusError; } @Override public Object apply(HttpResponse response, Class target) { + if (this.failOnStatusError) { + checkStatusCode(response); + } + Map result = new HashMap<>(); try { From 36cc08d912d558d66e9144ded1459fce16df7c5b Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Mon, 15 Sep 2025 16:46:37 +0100 Subject: [PATCH 18/27] fix RestWorkItemHandlerTest.java Signed-off-by: Pedro Escaleira --- .../java/org/kogito/workitem/rest/RestWorkItemHandler.java | 1 - .../rest/resulthandlers/DefaultRestWorkItemHandlerResult.java | 4 ++-- .../org/kogito/workitem/rest/RestWorkItemHandlerTest.java | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java index f7ea2d2ca44..6974e4335f5 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java @@ -68,7 +68,6 @@ import io.vertx.mutiny.ext.web.client.WebClient; import static org.kie.kogito.internal.utils.ConversionUtils.isEmpty; -import static org.kogito.workitem.rest.RestWorkItemHandlerUtils.checkStatusCode; import static org.kogito.workitem.rest.RestWorkItemHandlerUtils.getClassListParam; import static org.kogito.workitem.rest.RestWorkItemHandlerUtils.getClassParam; import static org.kogito.workitem.rest.RestWorkItemHandlerUtils.getParam; diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java index f4946a85a25..3c26e9657c9 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java @@ -18,8 +18,6 @@ */ package org.kogito.workitem.rest.resulthandlers; -import static org.kogito.workitem.rest.RestWorkItemHandlerUtils.checkStatusCode; - import java.util.HashMap; import java.util.Map; import java.util.Spliterators; @@ -34,6 +32,8 @@ import io.vertx.mutiny.core.buffer.Buffer; import io.vertx.mutiny.ext.web.client.HttpResponse; +import static org.kogito.workitem.rest.RestWorkItemHandlerUtils.checkStatusCode; + public class DefaultRestWorkItemHandlerResult implements RestWorkItemHandlerResult { public static final String STATUS_CODE_PARAM = "STATUS_CODE"; diff --git a/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/RestWorkItemHandlerTest.java b/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/RestWorkItemHandlerTest.java index c5fb7ba52aa..65c5597dbc5 100644 --- a/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/RestWorkItemHandlerTest.java +++ b/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/RestWorkItemHandlerTest.java @@ -158,7 +158,7 @@ public void init() { public void testEmptyInputModel() { ObjectMapper objectMapper = new ObjectMapper(); ObjectNode objectNode = objectMapper.createObjectNode().put("id", 26).put("name", "pepe"); - RestWorkItemHandlerResult resultHandler = new DefaultRestWorkItemHandlerResult(false, false); + RestWorkItemHandlerResult resultHandler = new DefaultRestWorkItemHandlerResult(false, false, true); HttpResponse response = mock(HttpResponse.class); when(response.statusCode()).thenReturn(200); when(response.bodyAsJson(ObjectNode.class)).thenReturn(objectNode); From f13a9e6eeebf680590a5da6b1bc2a78325815664 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Wed, 17 Sep 2025 17:49:42 +0100 Subject: [PATCH 19/27] unit tests Signed-off-by: Pedro Escaleira --- .../DefaultRestWorkItemHandlerResultTest.java | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResultTest.java diff --git a/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResultTest.java b/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResultTest.java new file mode 100644 index 00000000000..0c80d03ee58 --- /dev/null +++ b/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResultTest.java @@ -0,0 +1,183 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.kogito.workitem.rest.resulthandlers; + +import java.util.AbstractMap.SimpleEntry; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.vertx.core.json.DecodeException; +import io.vertx.mutiny.core.MultiMap; +import io.vertx.mutiny.core.buffer.Buffer; +import io.vertx.mutiny.ext.web.client.HttpResponse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class DefaultRestWorkItemHandlerResultTest { + @Mock + private HttpResponse response; + + @Mock + private MultiMap headers; + + private final ObjectMapper mapper = new ObjectMapper(); + + // --- Helper Methods --- + private void mockResponse(Buffer body, int statusCode, Map headersMap) throws Exception { + // Parse the buffer to JsonNode for consistent mocking + JsonNode jsonNode = mapper.readTree(body.toString()); + + // Mock bodyAsJson to return the parsed JSON structure + lenient().when(response.bodyAsJson(Map.class)).thenReturn(mapper.convertValue(jsonNode, Map.class)); + lenient().when(response.bodyAsJson(Object.class)).thenReturn(jsonNode); + lenient().when(response.bodyAsString()).thenReturn(body.toString()); + lenient().when(response.statusCode()).thenReturn(statusCode); + lenient().when(response.headers()).thenReturn(headers); + + // Mock the Consumer-based forEach method + lenient().doAnswer(invocation -> { + Consumer> action = invocation.getArgument(0); + for (Map.Entry entry : headersMap.entrySet()) { + action.accept(new SimpleEntry(entry)); + } + return null; + }).when(headers).forEach(ArgumentMatchers.>> any()); + } + + // --- Tests for `apply` --- + @Test + void apply_shouldReturnBodyOnly_whenNoFlagsSet() throws Exception { + DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, false, false); + mockResponse(Buffer.buffer("{\"key\":\"value\"}"), 200, Map.of()); + + Object result = handler.apply(response, null); + assertEquals(Map.of("key", "value"), result); + } + + @Test + void apply_shouldIncludeHeaders_whenReturnHeadersTrue() throws Exception { + DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(true, false, false); + mockResponse(Buffer.buffer("{\"data\":\"test\"}"), 200, Map.of("X-Custom", "header-value")); + + Map result = (Map) handler.apply(response, null); + System.out.printf("ADEUS " + result + "\n\n\n"); + assertEquals("test", result.get("data")); + assertEquals("header-value", result.get("HEADER_X-Custom")); + } + + @Test + void apply_shouldIncludeStatusCode_whenReturnStatusCodeTrue() throws Exception { + DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, true, false); + mockResponse(Buffer.buffer("{\"data\":\"test\"}"), 404, Map.of()); + + Map result = (Map) handler.apply(response, null); + assertEquals(404, result.get("STATUS_CODE")); + } + + @Test + void apply_shouldThrow_whenFailOnStatusErrorTrueAndNon2xx() { + DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, false, true); + when(response.statusCode()).thenReturn(500); + + assertThrows(RuntimeException.class, () -> handler.apply(response, null)); + } + + @Test + void apply_shouldHandleInvalidJson_whenBodyIsNotJson() throws Exception { + DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, false, false); + when(response.bodyAsJson(Map.class)).thenThrow(new DecodeException("Invalid JSON")); + when(response.bodyAsString()).thenReturn("raw text"); + + Map result = (Map) handler.apply(response, null); + assertEquals("raw text", result.get("body")); + } + + @Test + void apply_shouldExtractNestedJson_whenBodyIsComplex() throws Exception { + DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, false, false); + String complexJson = """ + { + "name": "Alice", + "age": 30, + "active": true, + "address": { + "city": "Lisbon", + "zip": 12345 + }, + "tags": ["a", "b", "c"] + } + """; + mockResponse(Buffer.buffer(complexJson), 200, Map.of()); + + Map result = (Map) handler.apply(response, null); + assertEquals("Alice", result.get("name")); + assertEquals(30, result.get("age")); + assertEquals(true, result.get("active")); + assertEquals(Map.of("city", "Lisbon", "zip", 12345), result.get("address")); + assertEquals(List.of("a", "b", "c"), result.get("tags")); + } + + @Test + void apply_shouldHandleArraysInJson_whenBodyContainsArray() throws Exception { + DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, false, false); + String arrayJson = "{\"items\":[1,2,3]}"; + mockResponse(Buffer.buffer(arrayJson), 200, Map.of()); + + Map result = (Map) handler.apply(response, null); + assertEquals(List.of(1, 2, 3), result.get("items")); + } + + @Test + void apply_shouldUseTargetClass_whenTargetIsProvided() throws Exception { + DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, false, false); + TestClass testObj = new TestClass("test", 123); + when(response.bodyAsJson(TestClass.class)).thenReturn(testObj); + + Object result = handler.apply(response, TestClass.class); + assertTrue(result instanceof TestClass); + assertEquals("test", ((TestClass) result).name); + assertEquals(123, ((TestClass) result).value); + } + + // --- Helper Classes --- + private static class TestClass { + private String name; + private int value; + + public TestClass(String name, int value) { + this.name = name; + this.value = value; + } + } +} From b5e68325829b711c967f8b4475ca53d180e7302f Mon Sep 17 00:00:00 2001 From: oEscal Date: Mon, 29 Sep 2025 17:53:44 +0100 Subject: [PATCH 20/27] some of the reviewer requests Signed-off-by: oEscal --- .../DefaultRestWorkItemHandlerResult.java | 49 +++++-------------- 1 file changed, 11 insertions(+), 38 deletions(-) diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java index 3c26e9657c9..e3a6f401e48 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java @@ -20,10 +20,8 @@ import java.util.HashMap; import java.util.Map; -import java.util.Spliterators; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; +import org.kie.kogito.jackson.utils.JsonObjectUtils; import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; import com.fasterxml.jackson.databind.JsonNode; @@ -34,13 +32,18 @@ import static org.kogito.workitem.rest.RestWorkItemHandlerUtils.checkStatusCode; + public class DefaultRestWorkItemHandlerResult implements RestWorkItemHandlerResult { public static final String STATUS_CODE_PARAM = "STATUS_CODE"; - private boolean returnHeaders = false; - private boolean returnStatusCode = false; - private boolean failOnStatusError = true; + private final boolean returnHeaders; + private final boolean returnStatusCode; + private final boolean failOnStatusError; + + public DefaultRestWorkItemHandlerResult() { + this(false, false, true); + } public DefaultRestWorkItemHandlerResult(boolean returnHeaders, boolean returnStatusCode, boolean failOnStatusError) { this.returnHeaders = returnHeaders; @@ -67,9 +70,9 @@ public Object apply(HttpResponse response, Class target) { ((Map) body).forEach((key, value) -> result.put(String.valueOf(key), value)); } else if (body instanceof JsonNode && ((JsonNode) body).isObject()) { JsonNode node = (JsonNode) body; - node.fields().forEachRemaining(entry -> result.put(entry.getKey(), extractJsonNodeValue(entry.getValue()))); + node.fields().forEachRemaining(entry -> result.put(entry.getKey(), JsonObjectUtils.toJavaValue(entry.getValue()))); } else { - result.put("body", body); + result.put("response", body); } } catch (DecodeException e) { result.put("body", response.bodyAsString()); @@ -84,34 +87,4 @@ public Object apply(HttpResponse response, Class target) { return result; } - - private static Object extractJsonNodeValue(JsonNode node) { - if (node.isTextual()) - return node.textValue(); - if (node.isInt()) - return node.intValue(); - if (node.isLong()) - return node.longValue(); - if (node.isDouble()) - return node.doubleValue(); - if (node.isBoolean()) - return node.booleanValue(); - if (node.isNull()) - return null; - if (node.isArray()) { - // Wrap the Iterator in a Spliterator and create a Stream - return StreamSupport.stream( - Spliterators.spliteratorUnknownSize(node.elements(), 0), - false) - .map(DefaultRestWorkItemHandlerResult::extractJsonNodeValue) - .collect(Collectors.toList()); - } - if (node.isObject()) { - // Handle objects by recursively processing each field - Map result = new HashMap<>(); - node.fields().forEachRemaining(entry -> result.put(entry.getKey(), extractJsonNodeValue(entry.getValue()))); - return result; - } - return node.toString(); - } } From 4da423ed1fc16e91cd233dd807dd4eb0296cf492 Mon Sep 17 00:00:00 2001 From: oEscal Date: Tue, 14 Oct 2025 21:08:02 +0100 Subject: [PATCH 21/27] requested restructure Signed-off-by: oEscal --- .../parser/types/RestTypeHandler.java | 9 +- .../workflow/rest/JsonNodeResultHandler.java | 29 ++- .../RestKogitoProcessContextResolver.java | 16 +- .../workitem/rest/RestWorkItemHandler.java | 12 +- .../decorators/AbstractParamsDecorator.java | 26 +-- .../decorators/PrefixParamsDecorator.java | 4 +- .../DefaultRestWorkItemHandlerResult.java | 59 +----- .../rest/RestWorkItemHandlerTest.java | 2 +- .../DefaultRestWorkItemHandlerResultTest.java | 183 ------------------ 9 files changed, 51 insertions(+), 289 deletions(-) delete mode 100644 kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResultTest.java diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-rest-parser/src/main/java/org/kie/kogito/serverless/workflow/parser/types/RestTypeHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-rest-parser/src/main/java/org/kie/kogito/serverless/workflow/parser/types/RestTypeHandler.java index 5740e735a85..aceca4c073f 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-rest-parser/src/main/java/org/kie/kogito/serverless/workflow/parser/types/RestTypeHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-rest-parser/src/main/java/org/kie/kogito/serverless/workflow/parser/types/RestTypeHandler.java @@ -45,7 +45,6 @@ public class RestTypeHandler extends WorkItemTypeHandler { public static final String REST_TYPE = "rest"; private static final String METHOD_SEPARATOR = ":"; private static final String PORT = "port"; - private static final String HOST = "host"; @Override protected > WorkItemNodeFactory fillWorkItemHandler(Workflow workflow, @@ -67,15 +66,9 @@ public class RestTypeHandler extends WorkItemTypeHandler { .workParameter(RestWorkItemHandler.METHOD, method) .workParameter(RestWorkItemHandler.USER, runtimeRestApi(functionDef, USER_PROP, context.getContext())) .workParameter(RestWorkItemHandler.PASSWORD, runtimeRestApi(functionDef, PASSWORD_PROP, context.getContext())) - .workParameter(RestWorkItemHandler.HOST, runtimeRestApi(functionDef, HOST, context.getContext())) + .workParameter(RestWorkItemHandler.HOST, runtimeRestApi(functionDef, "host", context.getContext())) .workParameter(RestWorkItemHandler.PORT, runtimeRestApi(functionDef, PORT, context.getContext(), Integer.class, context.getContext().getApplicationProperty(APP_PROPERTIES_FUNCTIONS_BASE + PORT).map(Integer::parseInt).orElse(null))) - .workParameter(RestWorkItemHandler.RETURN_HEADERS, runtimeRestApi(functionDef, RestWorkItemHandler.RETURN_HEADERS, context.getContext(), Boolean.class, - context.getContext().getApplicationProperty(APP_PROPERTIES_FUNCTIONS_BASE + RestWorkItemHandler.RETURN_HEADERS).map(Boolean::parseBoolean).orElse(false))) - .workParameter(RestWorkItemHandler.RETURN_STATUS_CODE, runtimeRestApi(functionDef, RestWorkItemHandler.RETURN_STATUS_CODE, context.getContext(), Boolean.class, - context.getContext().getApplicationProperty(APP_PROPERTIES_FUNCTIONS_BASE + RestWorkItemHandler.RETURN_STATUS_CODE).map(Boolean::parseBoolean).orElse(false))) - .workParameter(RestWorkItemHandler.FAIL_ON_STATUS_ERROR, runtimeRestApi(functionDef, RestWorkItemHandler.FAIL_ON_STATUS_ERROR, context.getContext(), Boolean.class, - context.getContext().getApplicationProperty(APP_PROPERTIES_FUNCTIONS_BASE + RestWorkItemHandler.FAIL_ON_STATUS_ERROR).map(Boolean::parseBoolean).orElse(true))) .workParameter(RestWorkItemHandler.BODY_BUILDER, new ParamsRestBodyBuilderSupplier()) .workParameter(BearerTokenAuthDecorator.BEARER_TOKEN, runtimeRestApi(functionDef, ACCESS_TOKEN, context.getContext())) .workParameter(ApiKeyAuthDecorator.KEY_PREFIX, runtimeRestApi(functionDef, API_KEY_PREFIX, context.getContext())) diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/JsonNodeResultHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/JsonNodeResultHandler.java index f91e71bfe57..c3f6c662529 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/JsonNodeResultHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/JsonNodeResultHandler.java @@ -33,18 +33,35 @@ public class JsonNodeResultHandler implements RestWorkItemHandlerResult { - static final String STATUS_CODE = "statusCode"; - static final String STATUS_MESSAGE = "statusMessage"; + public static final String RETURN_HEADERS = "returnHeaders"; + public static final String RETURN_STATUS_CODE = "returnStatusCode"; + public static final String RETURN_STATUS_MESSAGE = "returnStatusMessage"; + public static final String FAIL_ON_STATUS_ERROR = "failOnStatusCode"; + public static final String STATUS_CODE = "statusCode"; + public static final String STATUS_MESSAGE = "statusMessage"; + public static final String RESPONSE_HEADERS = "responseHeaders"; @Override public Object apply(HttpResponse t, Class u, KogitoProcessContext context) { Map metadata = context.getNodeInstance().getNode().getMetaData(); - if (metadata == null || toBoolean(metadata.getOrDefault("failOnStatusCode", Boolean.TRUE))) { + if (metadata == null || toBoolean(metadata.getOrDefault(FAIL_ON_STATUS_ERROR, Boolean.TRUE))) { checkStatusCode(t); - } else { - context.setVariable(STATUS_CODE, t.statusCode()); - context.setVariable(STATUS_MESSAGE, t.statusMessage()); } + + if (metadata != null) { + if (toBoolean(metadata.getOrDefault(RETURN_STATUS_CODE, Boolean.TRUE))) { + context.setVariable(STATUS_CODE, t.statusCode()); + } + + if (toBoolean(metadata.getOrDefault(RETURN_STATUS_MESSAGE, Boolean.TRUE))) { + context.setVariable(STATUS_MESSAGE, t.statusMessage()); + } + + if (toBoolean(metadata.getOrDefault(RETURN_HEADERS, Boolean.FALSE))) { + context.setVariable(RESPONSE_HEADERS, t.headers()); + } + } + return apply(t, u); } diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/RestKogitoProcessContextResolver.java b/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/RestKogitoProcessContextResolver.java index 85694ea880b..b1311309d6f 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/RestKogitoProcessContextResolver.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/RestKogitoProcessContextResolver.java @@ -24,13 +24,25 @@ import org.kie.kogito.internal.process.runtime.KogitoProcessContext; import org.kie.kogito.serverless.workflow.utils.KogitoProcessContextResolverExtension; +import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.FAIL_ON_STATUS_ERROR; +import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.RESPONSE_HEADERS; +import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.RETURN_HEADERS; +import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.RETURN_STATUS_CODE; +import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.RETURN_STATUS_MESSAGE; import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.STATUS_CODE; import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.STATUS_MESSAGE; public class RestKogitoProcessContextResolver implements KogitoProcessContextResolverExtension { + @Override public Map> getKogitoProcessContextResolver() { - return Map.of(JsonNodeResultHandler.STATUS_CODE, k -> k.getVariable(STATUS_CODE), - JsonNodeResultHandler.STATUS_MESSAGE, k -> k.getVariable(STATUS_MESSAGE)); + return Map.of( + JsonNodeResultHandler.RETURN_HEADERS, k -> k.getVariable(RETURN_HEADERS), + JsonNodeResultHandler.RETURN_STATUS_CODE, k -> k.getVariable(RETURN_STATUS_CODE), + JsonNodeResultHandler.RETURN_STATUS_MESSAGE, k -> k.getVariable(RETURN_STATUS_MESSAGE), + JsonNodeResultHandler.FAIL_ON_STATUS_ERROR, k -> k.getVariable(FAIL_ON_STATUS_ERROR), + JsonNodeResultHandler.STATUS_CODE, k -> k.getVariable(STATUS_CODE), + JsonNodeResultHandler.STATUS_MESSAGE, k -> k.getVariable(STATUS_MESSAGE), + JsonNodeResultHandler.RESPONSE_HEADERS, k -> k.getVariable(RESPONSE_HEADERS)); } } diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java index 6974e4335f5..8e7ff73d1f3 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/RestWorkItemHandler.java @@ -90,9 +90,6 @@ public class RestWorkItemHandler extends DefaultKogitoWorkItemHandler { public static final String PARAMS_DECORATOR = "ParamsDecorator"; public static final String PATH_PARAM_RESOLVER = "PathParamResolver"; public static final String AUTH_METHOD = "AuthMethod"; - public static final String RETURN_HEADERS = "return_headers"; - public static final String RETURN_STATUS_CODE = "return_status_code"; - public static final String FAIL_ON_STATUS_ERROR = "fail_on_status_error"; public static final String TARGET_TYPE = "TargetType"; public static final String REQUEST_TIMEOUT_IN_MILLIS = "RequestTimeout"; @@ -101,7 +98,7 @@ public class RestWorkItemHandler extends DefaultKogitoWorkItemHandler { public static final int DEFAULT_SSL_PORT = 443; private static final Logger logger = LoggerFactory.getLogger(RestWorkItemHandler.class); - private RestWorkItemHandlerResult DEFAULT_RESULT_HANDLER; + private static final RestWorkItemHandlerResult DEFAULT_RESULT_HANDLER = new DefaultRestWorkItemHandlerResult(); private static final RestWorkItemHandlerBodyBuilder DEFAULT_BODY_BUILDER = new DefaultWorkItemHandlerBodyBuilder(); private static final ParamsDecorator DEFAULT_PARAMS_DECORATOR = new PrefixParamsDecorator(); private static final PathParamResolver DEFAULT_PATH_PARAM_RESOLVER = new DefaultPathParamResolver(); @@ -137,12 +134,6 @@ public Optional activateWorkItemHandler(KogitoWorkItemManage throw new IllegalArgumentException("Missing required parameter " + URL); } - boolean failOnStatusError = getParam(parameters, FAIL_ON_STATUS_ERROR, Boolean.class, true); - - boolean returnHeaders = getParam(parameters, RETURN_HEADERS, Boolean.class, false); - boolean returnStatusCode = getParam(parameters, RETURN_STATUS_CODE, Boolean.class, false); - DEFAULT_RESULT_HANDLER = new DefaultRestWorkItemHandlerResult(returnHeaders, returnStatusCode, failOnStatusError); - HttpMethod method = getParam(parameters, METHOD, HttpMethod.class, HttpMethod.GET); RestWorkItemHandlerResult resultHandler = getClassParam(parameters, RESULT_HANDLER, RestWorkItemHandlerResult.class, DEFAULT_RESULT_HANDLER, resultHandlers); RestWorkItemHandlerBodyBuilder bodyBuilder = getClassParam(parameters, BODY_BUILDER, RestWorkItemHandlerBodyBuilder.class, DEFAULT_BODY_BUILDER, bodyBuilders); @@ -201,7 +192,6 @@ public Optional activateWorkItemHandler(KogitoWorkItemManage HttpResponse response = method.equals(HttpMethod.POST) || method.equals(HttpMethod.PUT) ? sendBody(request, bodyBuilder.apply(parameters), requestTimeout) : send(request, requestTimeout); - return Optional.of(this.workItemLifeCycle.newTransition("complete", workItem.getPhaseStatus(), Collections.singletonMap(RESULT, resultHandler.apply(response, targetInfo, ContextFactory.fromItem(workItem))))); } diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java index f51d2e5ae81..c88916322e6 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java @@ -18,11 +18,9 @@ */ package org.kogito.workitem.rest.decorators; -import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; -import java.util.Set; import org.kie.kogito.internal.process.workitem.KogitoWorkItem; @@ -32,36 +30,26 @@ public abstract class AbstractParamsDecorator implements ParamsDecorator { @Override public void decorate(KogitoWorkItem item, Map parameters, HttpRequest request) { - extractHeadersQueries(item, parameters, request); - } - - protected String toHeaderKey(String key) { - return key; - } - - protected String toQueryKey(String key) { - return key; - } - - protected Set extractHeadersQueries(KogitoWorkItem item, Map parameters, HttpRequest request) { - Set consideredParams = new HashSet<>(); - Iterator> iter = parameters.entrySet().iterator(); while (iter.hasNext()) { Entry entry = iter.next(); String key = entry.getKey(); if (isHeaderParameter(key)) { request.putHeader(toHeaderKey(key), entry.getValue().toString()); - consideredParams.add(key); iter.remove(); } else if (isQueryParameter(key)) { request.addQueryParam(toQueryKey(key), entry.getValue().toString()); - consideredParams.add(key); iter.remove(); } } + } - return consideredParams; + protected String toHeaderKey(String key) { + return key; + } + + protected String toQueryKey(String key) { + return key; } protected abstract boolean isHeaderParameter(String key); diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java index 191d08a9d76..1d88e84e9d9 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java @@ -20,8 +20,8 @@ public class PrefixParamsDecorator extends AbstractParamsDecorator { - public static final String HEADER_PREFIX = "HEADER_"; - public static final String QUERY_PREFIX = "QUERY_"; + private static final String HEADER_PREFIX = "HEADER_"; + private static final String QUERY_PREFIX = "QUERY_"; @Override protected boolean isHeaderParameter(String key) { diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java index e3a6f401e48..5317c076589 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResult.java @@ -18,73 +18,18 @@ */ package org.kogito.workitem.rest.resulthandlers; -import java.util.HashMap; import java.util.Map; -import org.kie.kogito.jackson.utils.JsonObjectUtils; -import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; - -import com.fasterxml.jackson.databind.JsonNode; - -import io.vertx.core.json.DecodeException; import io.vertx.mutiny.core.buffer.Buffer; import io.vertx.mutiny.ext.web.client.HttpResponse; import static org.kogito.workitem.rest.RestWorkItemHandlerUtils.checkStatusCode; - public class DefaultRestWorkItemHandlerResult implements RestWorkItemHandlerResult { - public static final String STATUS_CODE_PARAM = "STATUS_CODE"; - - private final boolean returnHeaders; - private final boolean returnStatusCode; - private final boolean failOnStatusError; - - public DefaultRestWorkItemHandlerResult() { - this(false, false, true); - } - - public DefaultRestWorkItemHandlerResult(boolean returnHeaders, boolean returnStatusCode, boolean failOnStatusError) { - this.returnHeaders = returnHeaders; - this.returnStatusCode = returnStatusCode; - this.failOnStatusError = failOnStatusError; - } - @Override public Object apply(HttpResponse response, Class target) { - if (this.failOnStatusError) { - checkStatusCode(response); - } - - Map result = new HashMap<>(); - - try { - Object body = target == null ? response.bodyAsJson(Map.class) : response.bodyAsJson(target); - - if (!this.returnHeaders && !this.returnStatusCode) { - return body; - } - - if (body instanceof Map) { - ((Map) body).forEach((key, value) -> result.put(String.valueOf(key), value)); - } else if (body instanceof JsonNode && ((JsonNode) body).isObject()) { - JsonNode node = (JsonNode) body; - node.fields().forEachRemaining(entry -> result.put(entry.getKey(), JsonObjectUtils.toJavaValue(entry.getValue()))); - } else { - result.put("response", body); - } - } catch (DecodeException e) { - result.put("body", response.bodyAsString()); - } - - if (this.returnHeaders) { - response.headers().forEach(entry -> result.put(PrefixParamsDecorator.HEADER_PREFIX + entry.getKey(), entry.getValue())); - } - if (this.returnStatusCode) { - result.put(STATUS_CODE_PARAM, response.statusCode()); - } - - return result; + checkStatusCode(response); + return target == null ? response.bodyAsJson(Map.class) : response.bodyAsJson(target); } } diff --git a/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/RestWorkItemHandlerTest.java b/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/RestWorkItemHandlerTest.java index 65c5597dbc5..a3054e743a8 100644 --- a/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/RestWorkItemHandlerTest.java +++ b/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/RestWorkItemHandlerTest.java @@ -158,7 +158,7 @@ public void init() { public void testEmptyInputModel() { ObjectMapper objectMapper = new ObjectMapper(); ObjectNode objectNode = objectMapper.createObjectNode().put("id", 26).put("name", "pepe"); - RestWorkItemHandlerResult resultHandler = new DefaultRestWorkItemHandlerResult(false, false, true); + RestWorkItemHandlerResult resultHandler = new DefaultRestWorkItemHandlerResult(); HttpResponse response = mock(HttpResponse.class); when(response.statusCode()).thenReturn(200); when(response.bodyAsJson(ObjectNode.class)).thenReturn(objectNode); diff --git a/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResultTest.java b/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResultTest.java deleted file mode 100644 index 0c80d03ee58..00000000000 --- a/kogito-workitems/kogito-rest-workitem/src/test/java/org/kogito/workitem/rest/resulthandlers/DefaultRestWorkItemHandlerResultTest.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 org.kogito.workitem.rest.resulthandlers; - -import java.util.AbstractMap.SimpleEntry; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentMatchers; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; - -import io.vertx.core.json.DecodeException; -import io.vertx.mutiny.core.MultiMap; -import io.vertx.mutiny.core.buffer.Buffer; -import io.vertx.mutiny.ext.web.client.HttpResponse; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -class DefaultRestWorkItemHandlerResultTest { - @Mock - private HttpResponse response; - - @Mock - private MultiMap headers; - - private final ObjectMapper mapper = new ObjectMapper(); - - // --- Helper Methods --- - private void mockResponse(Buffer body, int statusCode, Map headersMap) throws Exception { - // Parse the buffer to JsonNode for consistent mocking - JsonNode jsonNode = mapper.readTree(body.toString()); - - // Mock bodyAsJson to return the parsed JSON structure - lenient().when(response.bodyAsJson(Map.class)).thenReturn(mapper.convertValue(jsonNode, Map.class)); - lenient().when(response.bodyAsJson(Object.class)).thenReturn(jsonNode); - lenient().when(response.bodyAsString()).thenReturn(body.toString()); - lenient().when(response.statusCode()).thenReturn(statusCode); - lenient().when(response.headers()).thenReturn(headers); - - // Mock the Consumer-based forEach method - lenient().doAnswer(invocation -> { - Consumer> action = invocation.getArgument(0); - for (Map.Entry entry : headersMap.entrySet()) { - action.accept(new SimpleEntry(entry)); - } - return null; - }).when(headers).forEach(ArgumentMatchers.>> any()); - } - - // --- Tests for `apply` --- - @Test - void apply_shouldReturnBodyOnly_whenNoFlagsSet() throws Exception { - DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, false, false); - mockResponse(Buffer.buffer("{\"key\":\"value\"}"), 200, Map.of()); - - Object result = handler.apply(response, null); - assertEquals(Map.of("key", "value"), result); - } - - @Test - void apply_shouldIncludeHeaders_whenReturnHeadersTrue() throws Exception { - DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(true, false, false); - mockResponse(Buffer.buffer("{\"data\":\"test\"}"), 200, Map.of("X-Custom", "header-value")); - - Map result = (Map) handler.apply(response, null); - System.out.printf("ADEUS " + result + "\n\n\n"); - assertEquals("test", result.get("data")); - assertEquals("header-value", result.get("HEADER_X-Custom")); - } - - @Test - void apply_shouldIncludeStatusCode_whenReturnStatusCodeTrue() throws Exception { - DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, true, false); - mockResponse(Buffer.buffer("{\"data\":\"test\"}"), 404, Map.of()); - - Map result = (Map) handler.apply(response, null); - assertEquals(404, result.get("STATUS_CODE")); - } - - @Test - void apply_shouldThrow_whenFailOnStatusErrorTrueAndNon2xx() { - DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, false, true); - when(response.statusCode()).thenReturn(500); - - assertThrows(RuntimeException.class, () -> handler.apply(response, null)); - } - - @Test - void apply_shouldHandleInvalidJson_whenBodyIsNotJson() throws Exception { - DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, false, false); - when(response.bodyAsJson(Map.class)).thenThrow(new DecodeException("Invalid JSON")); - when(response.bodyAsString()).thenReturn("raw text"); - - Map result = (Map) handler.apply(response, null); - assertEquals("raw text", result.get("body")); - } - - @Test - void apply_shouldExtractNestedJson_whenBodyIsComplex() throws Exception { - DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, false, false); - String complexJson = """ - { - "name": "Alice", - "age": 30, - "active": true, - "address": { - "city": "Lisbon", - "zip": 12345 - }, - "tags": ["a", "b", "c"] - } - """; - mockResponse(Buffer.buffer(complexJson), 200, Map.of()); - - Map result = (Map) handler.apply(response, null); - assertEquals("Alice", result.get("name")); - assertEquals(30, result.get("age")); - assertEquals(true, result.get("active")); - assertEquals(Map.of("city", "Lisbon", "zip", 12345), result.get("address")); - assertEquals(List.of("a", "b", "c"), result.get("tags")); - } - - @Test - void apply_shouldHandleArraysInJson_whenBodyContainsArray() throws Exception { - DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, false, false); - String arrayJson = "{\"items\":[1,2,3]}"; - mockResponse(Buffer.buffer(arrayJson), 200, Map.of()); - - Map result = (Map) handler.apply(response, null); - assertEquals(List.of(1, 2, 3), result.get("items")); - } - - @Test - void apply_shouldUseTargetClass_whenTargetIsProvided() throws Exception { - DefaultRestWorkItemHandlerResult handler = new DefaultRestWorkItemHandlerResult(false, false, false); - TestClass testObj = new TestClass("test", 123); - when(response.bodyAsJson(TestClass.class)).thenReturn(testObj); - - Object result = handler.apply(response, TestClass.class); - assertTrue(result instanceof TestClass); - assertEquals("test", ((TestClass) result).name); - assertEquals(123, ((TestClass) result).value); - } - - // --- Helper Classes --- - private static class TestClass { - private String name; - private int value; - - public TestClass(String name, int value) { - this.name = name; - this.value = value; - } - } -} From ad843d23b3cb7da05465874fa9771776e4d6be79 Mon Sep 17 00:00:00 2001 From: oEscal Date: Fri, 17 Oct 2025 22:36:19 +0100 Subject: [PATCH 22/27] answer the reviewers comments and IT test Signed-off-by: oEscal --- .../workflow/rest/JsonNodeResultHandler.java | 21 +++---- .../RestKogitoProcessContextResolver.java | 8 --- .../src/main/resources/application.properties | 6 +- .../main/resources/workflowHeaders.sw.yaml | 49 +++++++++++++++ .../quarkus/workflows/WorkflowHeaders.java | 45 ++++++++++++++ .../workflows/WorkflowHeadersMock.java | 61 +++++++++++++++++++ 6 files changed, 167 insertions(+), 23 deletions(-) create mode 100644 quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/workflowHeaders.sw.yaml create mode 100644 quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/WorkflowHeaders.java create mode 100644 quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/WorkflowHeadersMock.java diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/JsonNodeResultHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/JsonNodeResultHandler.java index c3f6c662529..4d447192e96 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/JsonNodeResultHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/JsonNodeResultHandler.java @@ -18,6 +18,7 @@ */ package org.kie.kogito.serverless.workflow.rest; +import java.util.HashMap; import java.util.Map; import org.kie.kogito.internal.process.runtime.KogitoProcessContext; @@ -33,9 +34,6 @@ public class JsonNodeResultHandler implements RestWorkItemHandlerResult { - public static final String RETURN_HEADERS = "returnHeaders"; - public static final String RETURN_STATUS_CODE = "returnStatusCode"; - public static final String RETURN_STATUS_MESSAGE = "returnStatusMessage"; public static final String FAIL_ON_STATUS_ERROR = "failOnStatusCode"; public static final String STATUS_CODE = "statusCode"; public static final String STATUS_MESSAGE = "statusMessage"; @@ -46,20 +44,15 @@ public Object apply(HttpResponse t, Class u, KogitoProcessContext con Map metadata = context.getNodeInstance().getNode().getMetaData(); if (metadata == null || toBoolean(metadata.getOrDefault(FAIL_ON_STATUS_ERROR, Boolean.TRUE))) { checkStatusCode(t); + } else { + context.setVariable(STATUS_CODE, t.statusCode()); + context.setVariable(STATUS_MESSAGE, t.statusMessage()); } if (metadata != null) { - if (toBoolean(metadata.getOrDefault(RETURN_STATUS_CODE, Boolean.TRUE))) { - context.setVariable(STATUS_CODE, t.statusCode()); - } - - if (toBoolean(metadata.getOrDefault(RETURN_STATUS_MESSAGE, Boolean.TRUE))) { - context.setVariable(STATUS_MESSAGE, t.statusMessage()); - } - - if (toBoolean(metadata.getOrDefault(RETURN_HEADERS, Boolean.FALSE))) { - context.setVariable(RESPONSE_HEADERS, t.headers()); - } + Map headersMap = new HashMap<>(); + t.headers().forEach(entry -> headersMap.put(entry.getKey(), entry.getValue())); + context.setVariable(RESPONSE_HEADERS, headersMap); } return apply(t, u); diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/RestKogitoProcessContextResolver.java b/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/RestKogitoProcessContextResolver.java index b1311309d6f..969e9f64b92 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/RestKogitoProcessContextResolver.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/RestKogitoProcessContextResolver.java @@ -24,11 +24,7 @@ import org.kie.kogito.internal.process.runtime.KogitoProcessContext; import org.kie.kogito.serverless.workflow.utils.KogitoProcessContextResolverExtension; -import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.FAIL_ON_STATUS_ERROR; import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.RESPONSE_HEADERS; -import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.RETURN_HEADERS; -import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.RETURN_STATUS_CODE; -import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.RETURN_STATUS_MESSAGE; import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.STATUS_CODE; import static org.kie.kogito.serverless.workflow.rest.JsonNodeResultHandler.STATUS_MESSAGE; @@ -37,10 +33,6 @@ public class RestKogitoProcessContextResolver implements KogitoProcessContextRes @Override public Map> getKogitoProcessContextResolver() { return Map.of( - JsonNodeResultHandler.RETURN_HEADERS, k -> k.getVariable(RETURN_HEADERS), - JsonNodeResultHandler.RETURN_STATUS_CODE, k -> k.getVariable(RETURN_STATUS_CODE), - JsonNodeResultHandler.RETURN_STATUS_MESSAGE, k -> k.getVariable(RETURN_STATUS_MESSAGE), - JsonNodeResultHandler.FAIL_ON_STATUS_ERROR, k -> k.getVariable(FAIL_ON_STATUS_ERROR), JsonNodeResultHandler.STATUS_CODE, k -> k.getVariable(STATUS_CODE), JsonNodeResultHandler.STATUS_MESSAGE, k -> k.getVariable(STATUS_MESSAGE), JsonNodeResultHandler.RESPONSE_HEADERS, k -> k.getVariable(RESPONSE_HEADERS)); diff --git a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/application.properties b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/application.properties index 0000ec86d9f..4097a656c12 100644 --- a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/application.properties +++ b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/application.properties @@ -323,4 +323,8 @@ quarkus.http.auth.permission.default.policy=authenticated quarkus.security.users.embedded.enabled=true quarkus.security.users.embedded.plain-text=true quarkus.security.users.embedded.users.buddy=buddy -quarkus.log.category."org.apache.http".level=INFO \ No newline at end of file +quarkus.log.category."org.apache.http".level=INFO + + +# Workflow headers properties +kogito.sw.functions.workflowHeaders.host=localhost diff --git a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/workflowHeaders.sw.yaml b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/workflowHeaders.sw.yaml new file mode 100644 index 00000000000..04cbb3e058d --- /dev/null +++ b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/main/resources/workflowHeaders.sw.yaml @@ -0,0 +1,49 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# + +--- +id: workflowHeaders +version: '1.0' +name: Workflow headers example +description: An example of how to access the response headers +start: getRequest +functions: +- name: workflowHeaders + type: custom + operation: rest:get:/customheaders +states: +- name: getRequest + type: operation + actions: + - name: getRequestAction + functionRef: + refName: workflowHeaders + transition: headersHandling +- name: headersHandling + type: switch + dataConditions: + - condition: '$WORKFLOW.responseHeaders["X-Custom-Header"] == "MyHeaderValue"' + transition: headerMessage + defaultCondition: + end: true +- name: headerMessage + type: inject + data: + message: "X-Custom-Header had the value MyHeaderValue" + end: true diff --git a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/WorkflowHeaders.java b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/WorkflowHeaders.java new file mode 100644 index 00000000000..5364dfe6d2f --- /dev/null +++ b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/WorkflowHeaders.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.kie.kogito.quarkus.workflows; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusIntegrationTest; +import io.restassured.http.ContentType; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.equalTo; + +@QuarkusTestResource(WorkflowHeadersMock.class) +@QuarkusIntegrationTest +public class WorkflowHeaders { + + @Test + public void testCustomResourceWithHeaders() { + given() + .contentType(ContentType.JSON) + .accept(ContentType.JSON) + .when() + .post("/workflowHeaders") + .then() + .statusCode(201) + .body("workflowdata.message", equalTo("X-Custom-Header had the value MyHeaderValue")); + } +} diff --git a/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/WorkflowHeadersMock.java b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/WorkflowHeadersMock.java new file mode 100644 index 00000000000..8afefddddde --- /dev/null +++ b/quarkus/extensions/kogito-quarkus-serverless-workflow-extension/kogito-quarkus-serverless-workflow-integration-test/src/test/java/org/kie/kogito/quarkus/workflows/WorkflowHeadersMock.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.kie.kogito.quarkus.workflows; + +import java.util.Collections; +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.configureFor; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options; + +public class WorkflowHeadersMock implements QuarkusTestResourceLifecycleManager { + + public static final String WORKFLOW_HEADERS_MOCK_PORT = "workflow-headers-mock.port"; + + private WireMockServer wireMockServer; + + @Override + public Map start() { + wireMockServer = new WireMockServer(options().dynamicPort()); + wireMockServer.start(); + int port = wireMockServer.port(); + configureFor(port); + + // mock a successful invocation + stubFor(get("/customheaders") + .willReturn(aResponse() + .withHeader("X-Custom-Header", "MyHeaderValue"))); + + return Collections.singletonMap("kogito.sw.functions.workflowHeaders.port", Integer.toString(port)); + } + + @Override + public void stop() { + if (wireMockServer != null) { + wireMockServer.stop(); + } + } +} From f60f7237a6467f338727181688728d8d805c4f88 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Mon, 20 Oct 2025 13:59:52 +0100 Subject: [PATCH 23/27] Update kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/JsonNodeResultHandler.java Signed-off-by: oEscal --- .../serverless/workflow/rest/JsonNodeResultHandler.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/JsonNodeResultHandler.java b/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/JsonNodeResultHandler.java index 4d447192e96..4412f6cc6ab 100644 --- a/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/JsonNodeResultHandler.java +++ b/kogito-serverless-workflow/kogito-serverless-workflow-rest-runtime/src/main/java/org/kie/kogito/serverless/workflow/rest/JsonNodeResultHandler.java @@ -18,8 +18,8 @@ */ package org.kie.kogito.serverless.workflow.rest; -import java.util.HashMap; import java.util.Map; +import java.util.stream.Collectors; import org.kie.kogito.internal.process.runtime.KogitoProcessContext; import org.kie.kogito.jackson.utils.JsonObjectUtils; @@ -49,11 +49,7 @@ public Object apply(HttpResponse t, Class u, KogitoProcessContext con context.setVariable(STATUS_MESSAGE, t.statusMessage()); } - if (metadata != null) { - Map headersMap = new HashMap<>(); - t.headers().forEach(entry -> headersMap.put(entry.getKey(), entry.getValue())); - context.setVariable(RESPONSE_HEADERS, headersMap); - } + context.setVariable(RESPONSE_HEADERS, t.headers().entries().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); return apply(t, u); } From c0fdab3c8a4d10a3b9def823c412fd0f330ccfd3 Mon Sep 17 00:00:00 2001 From: oEscal Date: Mon, 27 Oct 2025 11:34:11 +0000 Subject: [PATCH 24/27] going to the previous code version and updating the knative result handler to use the JsonNodeResultHandlerSupplier Signed-off-by: oEscal --- .../customfunctions/KnativeTypeHandler.java | 7 +-- .../serving/customfunctions/Operation.java | 59 +------------------ .../PlainJsonKnativeParamsDecorator.java | 44 +------------- .../customfunctions/OperationTest.java | 28 +++------ 4 files changed, 14 insertions(+), 124 deletions(-) diff --git a/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java b/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java index 74cca6b8c53..5bec0059e73 100644 --- a/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java +++ b/quarkus/addons/knative/serving/deployment/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/deployment/customfunctions/KnativeTypeHandler.java @@ -34,6 +34,7 @@ import org.kie.kogito.serverless.workflow.parser.ParserContext; import org.kie.kogito.serverless.workflow.parser.VariableInfo; import org.kie.kogito.serverless.workflow.parser.types.WorkItemTypeHandler; +import org.kie.kogito.serverless.workflow.suppliers.JsonNodeResultHandlerSupplier; import org.kie.kogito.serverless.workflow.suppliers.ParamsRestBodyBuilderSupplier; import org.kogito.workitem.rest.RestWorkItemHandler; @@ -80,10 +81,7 @@ public class KnativeTypeHandler extends WorkItemTypeHandler { node.workParameter(KnativeWorkItemHandler.SERVICE_PROPERTY_NAME, operation.getService()) .workParameter(KnativeWorkItemHandler.PATH_PROPERTY_NAME, operation.getPath()) - .workParameter(RestWorkItemHandler.METHOD, operation.getHttpMethod()) - .workParameter(RestWorkItemHandler.RETURN_HEADERS, operation.returnHeaders()) - .workParameter(RestWorkItemHandler.RETURN_STATUS_CODE, operation.returnStatusCode()) - .workParameter(RestWorkItemHandler.FAIL_ON_STATUS_ERROR, operation.failOnStatusError()); + .workParameter(RestWorkItemHandler.METHOD, operation.getHttpMethod()); return addFunctionArgs(workflow, fillWorkItemHandler(workflow, context, node, functionDef), @@ -110,6 +108,7 @@ private static List getPayloadFields(FunctionRef functionRef) { context.getContext(), String.class, DEFAULT_REQUEST_TIMEOUT_VALUE); return node.workParameter(RestWorkItemHandler.BODY_BUILDER, new ParamsRestBodyBuilderSupplier()) + .workParameter(RestWorkItemHandler.RESULT_HANDLER, new JsonNodeResultHandlerSupplier()) .workParameter(RestWorkItemHandler.REQUEST_TIMEOUT_IN_MILLIS, requestTimeout) .metaData(TaskDescriptor.KEY_WORKITEM_TYPE, RestWorkItemHandler.REST_TASK_TYPE) .workName(KnativeWorkItemHandler.NAME); diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java index 3dfd49fda74..c5a431686ef 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/Operation.java @@ -38,12 +38,6 @@ public final class Operation { static final String METHOD_PARAMETER_NAME = "method"; - static final String RETURN_HEADERS_PARAMETER_NAME = "returnHeaders"; - - static final String RETURN_STATUS_CODE_PARAMETER_NAME = "returnStatusCode"; - - static final String FAIL_ON_STATUS_ERROR_PARAMETER_NAME = "failOnStatusError"; - private final String service; private final String path; @@ -52,20 +46,11 @@ public final class Operation { private final HttpMethod httpMethod; - private final boolean returnHeaders; - - private final boolean returnStatusCode; - - private final boolean failOnStatusError; - private Operation(Builder builder) { this.service = Objects.requireNonNull(builder.service); this.path = builder.path != null ? builder.path : "/"; this.isCloudEvent = builder.isCloudEvent; this.httpMethod = builder.httpMethod; - this.returnHeaders = builder.returnHeaders; - this.returnStatusCode = builder.returnStatusCode; - this.failOnStatusError = builder.failOnStatusError; validate(this); } @@ -96,18 +81,6 @@ public HttpMethod getHttpMethod() { return httpMethod; } - public boolean returnHeaders() { - return returnHeaders; - } - - public boolean returnStatusCode() { - return returnStatusCode; - } - - public boolean failOnStatusError() { - return failOnStatusError; - } - public static Operation parse(String value) { String[] parts = value.split("\\?", 2); @@ -123,9 +96,6 @@ public static Operation parse(String value) { .withPath(params.get(PATH_PARAMETER_NAME)) .withIsCloudEvent(Boolean.parseBoolean(params.get(CLOUD_EVENT_PARAMETER_NAME))) .withMethod(HttpMethod.valueOf(params.getOrDefault(METHOD_PARAMETER_NAME, DEFAULT_HTTP_METHOD.name()).toUpperCase())) - .withReturnHeaders(Boolean.parseBoolean(params.get(RETURN_HEADERS_PARAMETER_NAME))) - .withReturnStatusCode(Boolean.parseBoolean(params.get(RETURN_STATUS_CODE_PARAMETER_NAME))) - .withFailOnStatusError(Boolean.parseBoolean(params.getOrDefault(FAIL_ON_STATUS_ERROR_PARAMETER_NAME, "true"))) .build(); } @@ -143,9 +113,6 @@ public boolean equals(Object o) { } Operation operation = (Operation) o; return isCloudEvent == operation.isCloudEvent - && returnHeaders == operation.returnHeaders - && returnStatusCode == operation.returnStatusCode - && failOnStatusError == operation.failOnStatusError && Objects.equals(service, operation.service) && Objects.equals(path, operation.path) && Objects.equals(httpMethod, operation.httpMethod); @@ -158,15 +125,12 @@ public String toString() { ", path='" + path + '\'' + ", isCloudEvent=" + isCloudEvent + ", httpMethod=" + httpMethod + - ", returnHeaders=" + returnHeaders + - ", returnStatusCode=" + returnStatusCode + - ", failOnStatusError=" + failOnStatusError + '}'; } @Override public int hashCode() { - return Objects.hash(service, path, isCloudEvent, httpMethod, returnHeaders, returnStatusCode, failOnStatusError); + return Objects.hash(service, path, isCloudEvent, httpMethod); } public static class Builder { @@ -179,12 +143,6 @@ public static class Builder { private HttpMethod httpMethod = DEFAULT_HTTP_METHOD; - private boolean returnHeaders; - - private boolean returnStatusCode; - - private boolean failOnStatusError; - private Builder() { } @@ -208,21 +166,6 @@ public Builder withMethod(HttpMethod httpMethod) { return this; } - public Builder withReturnHeaders(boolean returnHeaders) { - this.returnHeaders = returnHeaders; - return this; - } - - public Builder withReturnStatusCode(boolean returnStatusCode) { - this.returnStatusCode = returnStatusCode; - return this; - } - - public Builder withFailOnStatusError(boolean failOnStatusError) { - this.failOnStatusError = failOnStatusError; - return this; - } - public Operation build() { return new Operation(this); } diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java index 1d6b5385cba..57ac0c33d14 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java @@ -18,25 +18,17 @@ */ package org.kie.kogito.addons.quarkus.knative.serving.customfunctions; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; import org.kie.kogito.event.cloudevents.utils.CloudEventUtils; import org.kie.kogito.internal.process.workitem.KogitoWorkItem; import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; - import io.vertx.mutiny.ext.web.client.HttpRequest; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.CLOUDEVENT_SENT_AS_PLAIN_JSON_ERROR_MESSAGE; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.ID; -import static org.kie.kogito.serverless.workflow.SWFConstants.MODEL_WORKFLOW_VAR; public final class PlainJsonKnativeParamsDecorator extends PrefixParamsDecorator { @@ -45,44 +37,12 @@ public void decorate(KogitoWorkItem workItem, Map parameters, Ht if (isCloudEvent(KnativeFunctionPayloadSupplier.getPayload(parameters))) { throw new IllegalArgumentException(CLOUDEVENT_SENT_AS_PLAIN_JSON_ERROR_MESSAGE); } - buildFromParams(workItem, parameters, request); + + super.decorate(workItem, parameters, request); } private static boolean isCloudEvent(Map payload) { List cloudEventMissingAttributes = CloudEventUtils.getMissingAttributes(payload); return !payload.isEmpty() && (cloudEventMissingAttributes.isEmpty() || (cloudEventMissingAttributes.size() == 1 && cloudEventMissingAttributes.contains(ID))); } - - private void buildFromParams(KogitoWorkItem workItem, Map parameters, HttpRequest request) { - Map inputModel = new HashMap<>(); - - Object inputModelObject = parameters.get(MODEL_WORKFLOW_VAR); - - ObjectNode inputModelCopy = null; - if (inputModelObject instanceof ObjectNode objectNode) { - ObjectMapper mapper = new ObjectMapper(); - objectNode.fields().forEachRemaining(entry -> { - JsonNode value = entry.getValue(); - Object rawValue = mapper.convertValue(value, Object.class); - inputModel.put(entry.getKey(), rawValue); - }); - - try { - inputModelCopy = (ObjectNode) mapper.readTree(objectNode.toString()); - } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to copy MODEL_WORKFLOW_VAR", e); - } - } - - Set paramsRemove = super.extractHeadersQueries(workItem, inputModel, request); - super.decorate(workItem, parameters, request); - - if (inputModelCopy != null) { - // mutate the safe copy - inputModelCopy.remove(paramsRemove); - - // replace the original entry in parameters with the copy - parameters.put(MODEL_WORKFLOW_VAR, inputModelCopy); - } - } } diff --git a/quarkus/addons/knative/serving/runtime/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/OperationTest.java b/quarkus/addons/knative/serving/runtime/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/OperationTest.java index c5cfee9e2a3..020dd47315d 100644 --- a/quarkus/addons/knative/serving/runtime/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/OperationTest.java +++ b/quarkus/addons/knative/serving/runtime/src/test/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/OperationTest.java @@ -29,11 +29,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.CLOUD_EVENT_PARAMETER_NAME; -import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.FAIL_ON_STATUS_ERROR_PARAMETER_NAME; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.METHOD_PARAMETER_NAME; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.PATH_PARAMETER_NAME; -import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.RETURN_HEADERS_PARAMETER_NAME; -import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.Operation.RETURN_STATUS_CODE_PARAMETER_NAME; class OperationTest { @@ -41,32 +38,23 @@ class OperationTest { public static Stream parseSource() { return Stream.of( - Arguments.of(SERVICE, Operation.builder().withService(SERVICE).withFailOnStatusError(true).build()), + Arguments.of(SERVICE, Operation.builder().withService(SERVICE).build()), - Arguments.of("service?", Operation.builder().withService(SERVICE).withFailOnStatusError(true).build()), + Arguments.of("service?", Operation.builder().withService(SERVICE).build()), - Arguments.of("service?" + PATH_PARAMETER_NAME + "=/my_path", Operation.builder().withService(SERVICE).withPath("/my_path").withFailOnStatusError(true).build()), + Arguments.of("service?" + PATH_PARAMETER_NAME + "=/my_path", Operation.builder().withService(SERVICE).withPath("/my_path").build()), - Arguments.of("service?" + CLOUD_EVENT_PARAMETER_NAME + "=true", Operation.builder().withService(SERVICE).withFailOnStatusError(true).withIsCloudEvent(true).build()), + Arguments.of("service?" + CLOUD_EVENT_PARAMETER_NAME + "=true", Operation.builder().withService(SERVICE).withIsCloudEvent(true).build()), - Arguments.of("service?" + METHOD_PARAMETER_NAME + "=GET", Operation.builder().withService(SERVICE).withFailOnStatusError(true).withMethod(HttpMethod.GET).build()), + Arguments.of("service?" + METHOD_PARAMETER_NAME + "=GET", Operation.builder().withService(SERVICE).withMethod(HttpMethod.GET).build()), - Arguments.of("service?" + METHOD_PARAMETER_NAME + "=get", Operation.builder().withService(SERVICE).withFailOnStatusError(true).withMethod(HttpMethod.GET).build()), + Arguments.of("service?" + METHOD_PARAMETER_NAME + "=get", Operation.builder().withService(SERVICE).withMethod(HttpMethod.GET).build()), Arguments.of("service?" + PATH_PARAMETER_NAME + "=/my_path&" + CLOUD_EVENT_PARAMETER_NAME + "=true", - Operation.builder().withService(SERVICE).withPath("/my_path").withFailOnStatusError(true).withIsCloudEvent(true).build()), + Operation.builder().withService(SERVICE).withPath("/my_path").withIsCloudEvent(true).build()), Arguments.of("service?" + PATH_PARAMETER_NAME + "=/my_path&" + CLOUD_EVENT_PARAMETER_NAME + "=false&" + METHOD_PARAMETER_NAME + "=GET", - Operation.builder().withService(SERVICE).withPath("/my_path").withFailOnStatusError(true).withIsCloudEvent(false).withMethod(HttpMethod.GET).build()), - - Arguments.of("service?" + FAIL_ON_STATUS_ERROR_PARAMETER_NAME + "=false", - Operation.builder().withService(SERVICE).withFailOnStatusError(false).build()), - - Arguments.of("service?" + RETURN_HEADERS_PARAMETER_NAME + "=true", - Operation.builder().withService(SERVICE).withFailOnStatusError(true).withReturnHeaders(true).build()), - - Arguments.of("service?" + RETURN_STATUS_CODE_PARAMETER_NAME + "=true", - Operation.builder().withService(SERVICE).withFailOnStatusError(true).withReturnStatusCode(true).build())); + Operation.builder().withService(SERVICE).withPath("/my_path").withIsCloudEvent(false).withMethod(HttpMethod.GET).build())); } public static Stream invalidOperationSource() { From cda741f3f18bfba590a80461ec556e5447143424 Mon Sep 17 00:00:00 2001 From: oEscal Date: Mon, 27 Oct 2025 14:25:47 +0000 Subject: [PATCH 25/27] going back to defining headers and query params Signed-off-by: oEscal --- .../decorators/AbstractParamsDecorator.java | 26 ++++++++--- .../decorators/PrefixParamsDecorator.java | 4 +- .../PlainJsonKnativeParamsDecorator.java | 44 ++++++++++++++++++- 3 files changed, 63 insertions(+), 11 deletions(-) diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java index c88916322e6..f51d2e5ae81 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/AbstractParamsDecorator.java @@ -18,9 +18,11 @@ */ package org.kogito.workitem.rest.decorators; +import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import org.kie.kogito.internal.process.workitem.KogitoWorkItem; @@ -30,26 +32,36 @@ public abstract class AbstractParamsDecorator implements ParamsDecorator { @Override public void decorate(KogitoWorkItem item, Map parameters, HttpRequest request) { + extractHeadersQueries(item, parameters, request); + } + + protected String toHeaderKey(String key) { + return key; + } + + protected String toQueryKey(String key) { + return key; + } + + protected Set extractHeadersQueries(KogitoWorkItem item, Map parameters, HttpRequest request) { + Set consideredParams = new HashSet<>(); + Iterator> iter = parameters.entrySet().iterator(); while (iter.hasNext()) { Entry entry = iter.next(); String key = entry.getKey(); if (isHeaderParameter(key)) { request.putHeader(toHeaderKey(key), entry.getValue().toString()); + consideredParams.add(key); iter.remove(); } else if (isQueryParameter(key)) { request.addQueryParam(toQueryKey(key), entry.getValue().toString()); + consideredParams.add(key); iter.remove(); } } - } - protected String toHeaderKey(String key) { - return key; - } - - protected String toQueryKey(String key) { - return key; + return consideredParams; } protected abstract boolean isHeaderParameter(String key); diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java index 1d88e84e9d9..191d08a9d76 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java @@ -20,8 +20,8 @@ public class PrefixParamsDecorator extends AbstractParamsDecorator { - private static final String HEADER_PREFIX = "HEADER_"; - private static final String QUERY_PREFIX = "QUERY_"; + public static final String HEADER_PREFIX = "HEADER_"; + public static final String QUERY_PREFIX = "QUERY_"; @Override protected boolean isHeaderParameter(String key) { diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java index 57ac0c33d14..1d6b5385cba 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java @@ -18,17 +18,25 @@ */ package org.kie.kogito.addons.quarkus.knative.serving.customfunctions; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import org.kie.kogito.event.cloudevents.utils.CloudEventUtils; import org.kie.kogito.internal.process.workitem.KogitoWorkItem; import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + import io.vertx.mutiny.ext.web.client.HttpRequest; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.CLOUDEVENT_SENT_AS_PLAIN_JSON_ERROR_MESSAGE; import static org.kie.kogito.addons.quarkus.knative.serving.customfunctions.KnativeWorkItemHandler.ID; +import static org.kie.kogito.serverless.workflow.SWFConstants.MODEL_WORKFLOW_VAR; public final class PlainJsonKnativeParamsDecorator extends PrefixParamsDecorator { @@ -37,12 +45,44 @@ public void decorate(KogitoWorkItem workItem, Map parameters, Ht if (isCloudEvent(KnativeFunctionPayloadSupplier.getPayload(parameters))) { throw new IllegalArgumentException(CLOUDEVENT_SENT_AS_PLAIN_JSON_ERROR_MESSAGE); } - - super.decorate(workItem, parameters, request); + buildFromParams(workItem, parameters, request); } private static boolean isCloudEvent(Map payload) { List cloudEventMissingAttributes = CloudEventUtils.getMissingAttributes(payload); return !payload.isEmpty() && (cloudEventMissingAttributes.isEmpty() || (cloudEventMissingAttributes.size() == 1 && cloudEventMissingAttributes.contains(ID))); } + + private void buildFromParams(KogitoWorkItem workItem, Map parameters, HttpRequest request) { + Map inputModel = new HashMap<>(); + + Object inputModelObject = parameters.get(MODEL_WORKFLOW_VAR); + + ObjectNode inputModelCopy = null; + if (inputModelObject instanceof ObjectNode objectNode) { + ObjectMapper mapper = new ObjectMapper(); + objectNode.fields().forEachRemaining(entry -> { + JsonNode value = entry.getValue(); + Object rawValue = mapper.convertValue(value, Object.class); + inputModel.put(entry.getKey(), rawValue); + }); + + try { + inputModelCopy = (ObjectNode) mapper.readTree(objectNode.toString()); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to copy MODEL_WORKFLOW_VAR", e); + } + } + + Set paramsRemove = super.extractHeadersQueries(workItem, inputModel, request); + super.decorate(workItem, parameters, request); + + if (inputModelCopy != null) { + // mutate the safe copy + inputModelCopy.remove(paramsRemove); + + // replace the original entry in parameters with the copy + parameters.put(MODEL_WORKFLOW_VAR, inputModelCopy); + } + } } From 6bf42184ea137c75cc2e4371f148c59463157de0 Mon Sep 17 00:00:00 2001 From: Pedro Escaleira Date: Sat, 14 Mar 2026 03:34:00 +0000 Subject: [PATCH 26/27] Change header and query prefix back to private access --- .../workitem/rest/decorators/PrefixParamsDecorator.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java index 191d08a9d76..1d88e84e9d9 100644 --- a/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java +++ b/kogito-workitems/kogito-rest-workitem/src/main/java/org/kogito/workitem/rest/decorators/PrefixParamsDecorator.java @@ -20,8 +20,8 @@ public class PrefixParamsDecorator extends AbstractParamsDecorator { - public static final String HEADER_PREFIX = "HEADER_"; - public static final String QUERY_PREFIX = "QUERY_"; + private static final String HEADER_PREFIX = "HEADER_"; + private static final String QUERY_PREFIX = "QUERY_"; @Override protected boolean isHeaderParameter(String key) { From 481b736771d658a323cbfc20ff53b1519efd4315 Mon Sep 17 00:00:00 2001 From: oEscal Date: Tue, 17 Mar 2026 17:55:20 +0000 Subject: [PATCH 27/27] improve PlainJsonKnativeParamsDecorator implementation Signed-off-by: oEscal --- .../PlainJsonKnativeParamsDecorator.java | 50 +++++++++---------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java index 1d6b5385cba..02d68944b30 100644 --- a/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java +++ b/quarkus/addons/knative/serving/runtime/src/main/java/org/kie/kogito/addons/quarkus/knative/serving/customfunctions/PlainJsonKnativeParamsDecorator.java @@ -27,8 +27,6 @@ import org.kie.kogito.internal.process.workitem.KogitoWorkItem; import org.kogito.workitem.rest.decorators.PrefixParamsDecorator; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -54,35 +52,33 @@ private static boolean isCloudEvent(Map payload) { } private void buildFromParams(KogitoWorkItem workItem, Map parameters, HttpRequest request) { - Map inputModel = new HashMap<>(); + Object modelVar = parameters.get(MODEL_WORKFLOW_VAR); - Object inputModelObject = parameters.get(MODEL_WORKFLOW_VAR); - - ObjectNode inputModelCopy = null; - if (inputModelObject instanceof ObjectNode objectNode) { - ObjectMapper mapper = new ObjectMapper(); - objectNode.fields().forEachRemaining(entry -> { - JsonNode value = entry.getValue(); - Object rawValue = mapper.convertValue(value, Object.class); - inputModel.put(entry.getKey(), rawValue); - }); - - try { - inputModelCopy = (ObjectNode) mapper.readTree(objectNode.toString()); - } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to copy MODEL_WORKFLOW_VAR", e); - } + if (!(modelVar instanceof ObjectNode objectNode)) { + super.decorate(workItem, parameters, request); + return; } - Set paramsRemove = super.extractHeadersQueries(workItem, inputModel, request); - super.decorate(workItem, parameters, request); + // Flatten the ObjectNode into a plain map so extractHeadersQueries + // can remove header/query keys from it without touching the original + Map flatModel = flattenObjectNode(objectNode); - if (inputModelCopy != null) { - // mutate the safe copy - inputModelCopy.remove(paramsRemove); + // Extract headers/queries from the flat map (mutates it and the request) + Set extractedKeys = super.extractHeadersQueries(workItem, flatModel, request); - // replace the original entry in parameters with the copy - parameters.put(MODEL_WORKFLOW_VAR, inputModelCopy); - } + // Apply the same removals to a copy of the original ObjectNode, + // then put the sanitized copy back so downstream sees a clean model + ObjectNode sanitizedModel = objectNode.deepCopy(); + sanitizedModel.remove(extractedKeys); + parameters.put(MODEL_WORKFLOW_VAR, sanitizedModel); + } + + private Map flattenObjectNode(ObjectNode objectNode) { + ObjectMapper mapper = new ObjectMapper(); + Map result = new HashMap<>(); + objectNode.properties().forEach(entry -> + result.put(entry.getKey(), mapper.convertValue(entry.getValue(), Object.class)) + ); + return result; } }