diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractHttpExecutorBuilder.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractHttpExecutorBuilder.java index 18eb64a29..89e76e977 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractHttpExecutorBuilder.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractHttpExecutorBuilder.java @@ -46,9 +46,6 @@ protected static RequestSupplier buildRequestSupplier( return new WithoutBodyRequestSupplier(Invocation.Builder::head, application, redirect); case HttpMethod.OPTIONS: return new WithoutBodyRequestSupplier(Invocation.Builder::options, application, redirect); - case HttpMethod.PATCH: - return new WithBodyRequestSupplier( - (request, entity) -> request.method("patch", entity), application, body, redirect); case HttpMethod.GET: default: return new WithoutBodyRequestSupplier(Invocation.Builder::get, application, redirect); diff --git a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestSupplier.java b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestSupplier.java index 12a6c5a9d..d80b646e1 100644 --- a/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestSupplier.java +++ b/impl/http/src/main/java/io/serverlessworkflow/impl/executors/http/AbstractRequestSupplier.java @@ -15,6 +15,9 @@ */ package io.serverlessworkflow.impl.executors.http; +import static jakarta.ws.rs.core.Response.Status.Family.REDIRECTION; +import static jakarta.ws.rs.core.Response.Status.Family.SUCCESSFUL; + import io.serverlessworkflow.impl.TaskContext; import io.serverlessworkflow.impl.WorkflowContext; import io.serverlessworkflow.impl.WorkflowError; @@ -36,8 +39,14 @@ public AbstractRequestSupplier(boolean redirect) { public WorkflowModel apply( Builder request, WorkflowContext workflow, TaskContext task, WorkflowModel model) { HttpModelConverter converter = HttpConverterResolver.converter(workflow, task); + + if (!redirect) { + // disable automatic redirects handling from Jersey client + request.property("jersey.config.client.followRedirects", false); + } + Response response = invokeRequest(request, converter, workflow, task, model); - validateStatus(task, response, converter); + validateStatus(task, response); return workflow .definition() .application() @@ -45,11 +54,24 @@ public WorkflowModel apply( .fromAny(response.readEntity(converter.responseType())); } - private void validateStatus(TaskContext task, Response response, HttpModelConverter converter) { - if (response.getStatusInfo().getFamily() != Family.SUCCESSFUL) { + private void validateStatus(TaskContext task, Response response) { + Family statusFamily = response.getStatusInfo().getFamily(); + boolean isSuccess = statusFamily.equals(SUCCESSFUL); + boolean isRedirect = statusFamily.equals(REDIRECTION); + boolean valid = redirect ? (isSuccess || isRedirect) : isSuccess; + + if (!valid) { + String expectedRange = redirect ? "200-399" : "200-299"; throw new WorkflowException( - converter - .errorFromResponse(WorkflowError.communication(response.getStatus(), task), response) + WorkflowError.communication( + response.getStatus(), + task, + String.format( + "The property 'redirect' is set to %s but received status %d (%s); expected status in the %s range", + redirect, + response.getStatus(), + response.getStatusInfo().getReasonPhrase(), + expectedRange)) .build()); } } diff --git a/impl/test/src/test/java/io/serverlessworkflow/impl/test/HTTPWorkflowDefinitionTest.java b/impl/test/src/test/java/io/serverlessworkflow/impl/test/HTTPWorkflowDefinitionTest.java index 360f691c1..3591acaa2 100644 --- a/impl/test/src/test/java/io/serverlessworkflow/impl/test/HTTPWorkflowDefinitionTest.java +++ b/impl/test/src/test/java/io/serverlessworkflow/impl/test/HTTPWorkflowDefinitionTest.java @@ -18,23 +18,27 @@ import static io.serverlessworkflow.api.WorkflowReader.readWorkflowFromClasspath; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowableOfType; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import io.serverlessworkflow.impl.WorkflowApplication; import io.serverlessworkflow.impl.WorkflowModel; import java.io.IOException; import java.util.Map; -import java.util.stream.Stream; +import java.util.concurrent.CompletionException; +import mockwebserver3.MockResponse; +import mockwebserver3.MockWebServer; +import okhttp3.Headers; import org.assertj.core.api.Condition; import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; public class HTTPWorkflowDefinitionTest { private static WorkflowApplication appl; + private static MockWebServer mockServer; @BeforeAll static void init() { @@ -46,75 +50,205 @@ static void cleanup() { appl.close(); } - @ParameterizedTest - @MethodSource("provideParameters") - void testWorkflowExecution(String fileName, Object input, Condition condition) - throws IOException { - assertThat( - appl.workflowDefinition(readWorkflowFromClasspath(fileName)) - .instance(input) - .start() - .join()) - .is(condition); - } - - @ParameterizedTest - @ValueSource( - strings = { - "workflows-samples/call-http-query-parameters.yaml", - "workflows-samples/call-http-query-parameters-external-schema.yaml" - }) - void testWrongSchema(String fileName) { + @BeforeEach + void setup() throws IOException { + mockServer = new MockWebServer(); + mockServer.start(9876); + } + + @AfterEach + void shutdownServer() { + mockServer.close(); + } + + private static boolean httpCondition(WorkflowModel obj) { + Map map = obj.asMap().orElseThrow(); + return map.containsKey("photoUrls") || map.containsKey("petId"); + } + + @Test + void callHttpGet_should_return_pet_data() throws Exception { + WorkflowModel result = + appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-get.yaml")) + .instance(Map.of("petId", 10)) + .start() + .join(); + assertThat(result) + .has(new Condition<>(HTTPWorkflowDefinitionTest::httpCondition, "callHttpCondition")); + } + + @Test + void callHttpGet_with_not_found_petId_should_keep_input_petId() throws Exception { + WorkflowModel result = + appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-get.yaml")) + .instance(Map.of("petId", "-1")) + .start() + .join(); + assertThat(result.asMap().orElseThrow()).containsKey("petId"); + } + + @Test + void callHttpEndpointInterpolation_should_work() throws Exception { + WorkflowModel result = + appl.workflowDefinition( + readWorkflowFromClasspath( + "workflows-samples/call-http-endpoint-interpolation.yaml")) + .instance(Map.of("petId", 10)) + .start() + .join(); + assertThat(result) + .has(new Condition<>(HTTPWorkflowDefinitionTest::httpCondition, "callHttpCondition")); + } + + @Test + void callHttpQueryParameters_should_find_star_trek_movie() throws Exception { + WorkflowModel result = + appl.workflowDefinition( + readWorkflowFromClasspath("workflows-samples/call-http-query-parameters.yaml")) + .instance(Map.of("uid", "MOMA0000092393")) + .start() + .join(); + assertThat(((Map) result.asMap().orElseThrow().get("movie")).get("title")) + .isEqualTo("Star Trek"); + } + + @Test + void callHttpFindByStatus_should_return_non_empty_collection() throws Exception { + + WorkflowModel result = + appl.workflowDefinition( + readWorkflowFromClasspath("workflows-samples/call-http-find-by-status.yaml")) + .instance(Map.of()) + .start() + .join(); + assertThat(result.asCollection()).isNotEmpty(); + } + + @Test + void callHttpQueryParameters_external_schema_should_find_star_trek() throws Exception { + WorkflowModel result = + appl.workflowDefinition( + readWorkflowFromClasspath( + "workflows-samples/call-http-query-parameters-external-schema.yaml")) + .instance(Map.of("uid", "MOMA0000092393")) + .start() + .join(); + assertThat(((Map) result.asMap().orElseThrow().get("movie")).get("title")) + .isEqualTo("Star Trek"); + } + + @Test + void callHttpPost_should_return_created_firstName() throws Exception { + mockServer.enqueue( + new MockResponse( + 200, + Headers.of("Content-Type", "application/json"), + """ + { + "firstName": "Javierito" + } + """)); + WorkflowModel result = + appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-post.yaml")) + .instance(Map.of("name", "Javierito", "surname", "Unknown")) + .start() + .join(); + assertThat(result.asText().orElseThrow()).isEqualTo("Javierito"); + } + + @Test + void testCallHttpDelete() { + assertDoesNotThrow( + () -> { + appl.workflowDefinition( + readWorkflowFromClasspath("workflows-samples/call-http-delete.yaml")) + .instance(Map.of()) + .start() + .join(); + }); + } + + @Test + void callHttpPut_should_contain_firstName_with_john() throws Exception { + WorkflowModel result = + appl.workflowDefinition(readWorkflowFromClasspath("workflows-samples/call-http-put.yaml")) + .instance(Map.of("firstName", "John")) + .start() + .join(); + assertThat(result.asText().orElseThrow()).contains("John"); + } + + @Test + void testWrongSchema_should_throw_illegal_argument() { IllegalArgumentException exception = catchThrowableOfType( IllegalArgumentException.class, - () -> appl.workflowDefinition(readWorkflowFromClasspath(fileName)).instance(Map.of())); + () -> + appl.workflowDefinition( + readWorkflowFromClasspath( + "workflows-samples/call-http-query-parameters.yaml")) + .instance(Map.of())); assertThat(exception) .isNotNull() .hasMessageContaining("There are JsonSchema validation errors"); } - private static boolean httpCondition(WorkflowModel obj) { - Map map = obj.asMap().orElseThrow(); - return map.containsKey("photoUrls") || map.containsKey("petId"); + @Test + void testHeadCall() { + mockServer.enqueue( + new MockResponse( + 200, + Headers.of( + Map.of( + "Content-Length", + "123", + "Content-Type", + "application/json", + "X-Custom-Header", + "CustomValue")), + "")); + assertDoesNotThrow( + () -> { + appl.workflowDefinition( + readWorkflowFromClasspath("workflows-samples/call-http-head.yaml")) + .instance(Map.of()) + .start() + .join(); + }); } - private static Stream provideParameters() { - Map petInput = Map.of("petId", 10); - Map starTrekInput = Map.of("uid", "MOMA0000092393"); - Condition petCondition = - new Condition<>(HTTPWorkflowDefinitionTest::httpCondition, "callHttpCondition"); - Condition starTrekCondition = - new Condition<>( - o -> - ((Map) o.asMap().orElseThrow().get("movie")) - .get("title") - .equals("Star Trek"), - "StartTrek"); - Condition postCondition = - new Condition( - o -> o.asText().orElseThrow().equals("Javierito"), "CallHttpPostCondition"); - Map postMap = Map.of("name", "Javierito", "surname", "Unknown"); - return Stream.of( - Arguments.of("workflows-samples/callGetHttp.yaml", petInput, petCondition), - Arguments.of( - "workflows-samples/callGetHttp.yaml", - Map.of("petId", "-1"), - new Condition( - o -> o.asMap().orElseThrow().containsKey("petId"), "notFoundCondition")), - Arguments.of( - "workflows-samples/call-http-endpoint-interpolation.yaml", petInput, petCondition), - Arguments.of( - "workflows-samples/call-http-query-parameters.yaml", starTrekInput, starTrekCondition), - Arguments.of( - "workflows-samples/callFindByStatusHttp.yaml", - Map.of(), - new Condition(o -> !o.asCollection().isEmpty(), "HasElementCondition")), - Arguments.of( - "workflows-samples/call-http-query-parameters-external-schema.yaml", - starTrekInput, - starTrekCondition), - Arguments.of("workflows-samples/callPostHttp.yaml", postMap, postCondition), - Arguments.of("workflows-samples/callPostHttpAsExpr.yaml", postMap, postCondition)); + @Test + void testOptionsCall() { + mockServer.enqueue(new MockResponse(200, Headers.of("Allow", "GET, POST, OPTIONS"), "")); + + assertDoesNotThrow( + () -> { + appl.workflowDefinition( + readWorkflowFromClasspath("workflows-samples/call-http-options.yaml")) + .instance(Map.of()) + .start() + .join(); + }); + } + + @Test + void testRedirectAsFalse() { + mockServer.enqueue( + new MockResponse(301, Headers.of("Location", "http://localhost:9876/redirected"), "")); + + CompletionException exception = + catchThrowableOfType( + CompletionException.class, + () -> + appl.workflowDefinition( + readWorkflowFromClasspath( + "workflows-samples/call-http-redirect-false.yaml")) + .instance(Map.of()) + .start() + .join()); + + assertThat(exception.getCause().getMessage()) + .contains( + "The property 'redirect' is set to false but received status 301 (Redirection); expected status in the 200-299 range"); } } diff --git a/impl/test/src/test/resources/workflows-samples/call-http-delete.yaml b/impl/test/src/test/resources/workflows-samples/call-http-delete.yaml new file mode 100644 index 000000000..fe36413a2 --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/call-http-delete.yaml @@ -0,0 +1,12 @@ +document: + dsl: 1.0.0-alpha1 + namespace: test + name: call-http-delete + version: 1.0.0 +do: + - deleteAuthor: + call: http + with: + method: delete + endpoint: + uri: https://fakerestapi.azurewebsites.net/api/v1/Authors/1 diff --git a/impl/test/src/test/resources/workflows-samples/callFindByStatusHttp.yaml b/impl/test/src/test/resources/workflows-samples/call-http-find-by-status.yaml similarity index 93% rename from impl/test/src/test/resources/workflows-samples/callFindByStatusHttp.yaml rename to impl/test/src/test/resources/workflows-samples/call-http-find-by-status.yaml index 6adeed8c7..5f1928e16 100644 --- a/impl/test/src/test/resources/workflows-samples/callFindByStatusHttp.yaml +++ b/impl/test/src/test/resources/workflows-samples/call-http-find-by-status.yaml @@ -1,7 +1,7 @@ document: dsl: 1.0.0-alpha1 namespace: test - name: http-call-find-by-status + name: call-http-find-by-status version: 1.0.0 do: - tryGetPet: diff --git a/impl/test/src/test/resources/workflows-samples/callGetHttp.yaml b/impl/test/src/test/resources/workflows-samples/call-http-get.yaml similarity index 93% rename from impl/test/src/test/resources/workflows-samples/callGetHttp.yaml rename to impl/test/src/test/resources/workflows-samples/call-http-get.yaml index 192b0bcdc..54573d036 100644 --- a/impl/test/src/test/resources/workflows-samples/callGetHttp.yaml +++ b/impl/test/src/test/resources/workflows-samples/call-http-get.yaml @@ -1,7 +1,7 @@ document: dsl: 1.0.0-alpha1 namespace: test - name: http-call-with-response + name: call-http-with-response version: 1.0.0 do: - tryGetPet: diff --git a/impl/test/src/test/resources/workflows-samples/call-http-head.yaml b/impl/test/src/test/resources/workflows-samples/call-http-head.yaml new file mode 100644 index 000000000..df75c8a88 --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/call-http-head.yaml @@ -0,0 +1,12 @@ +document: + dsl: 1.0.0-alpha1 + namespace: test + name: call-http-head + version: 1.0.0 +do: + - useHead: + call: http + with: + method: head + endpoint: + uri: http://localhost:9876/users/1 \ No newline at end of file diff --git a/impl/test/src/test/resources/workflows-samples/call-http-options.yaml b/impl/test/src/test/resources/workflows-samples/call-http-options.yaml new file mode 100644 index 000000000..e209f0d50 --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/call-http-options.yaml @@ -0,0 +1,12 @@ +document: + dsl: 1.0.0-alpha1 + namespace: test + name: call-http-options + version: 1.0.0 +do: + - useOptions: + call: http + with: + method: options + endpoint: + uri: http://localhost:9876/users/1 \ No newline at end of file diff --git a/impl/test/src/test/resources/workflows-samples/callPostHttpAsExpr.yaml b/impl/test/src/test/resources/workflows-samples/call-http-post-expr.yaml similarity index 63% rename from impl/test/src/test/resources/workflows-samples/callPostHttpAsExpr.yaml rename to impl/test/src/test/resources/workflows-samples/call-http-post-expr.yaml index 69b7faac3..cb6830597 100644 --- a/impl/test/src/test/resources/workflows-samples/callPostHttpAsExpr.yaml +++ b/impl/test/src/test/resources/workflows-samples/call-http-post-expr.yaml @@ -1,15 +1,16 @@ document: dsl: 1.0.0-alpha1 namespace: test - name: http-call-with-response-output-expr + name: cal-http-with-body-and-response-with-expr version: 1.0.0 do: - - postPet: + - postUsers: call: http with: + redirect: false method: post endpoint: - uri: https://fakerestapi.azurewebsites.net/api/v1/Authors + uri: http://localhost:9876/users body: "${{firstName: .name, lastName:.surname}}" output: as: .firstName \ No newline at end of file diff --git a/impl/test/src/test/resources/workflows-samples/callPostHttp.yaml b/impl/test/src/test/resources/workflows-samples/call-http-post.yaml similarity index 84% rename from impl/test/src/test/resources/workflows-samples/callPostHttp.yaml rename to impl/test/src/test/resources/workflows-samples/call-http-post.yaml index f12fec423..0f1de3290 100644 --- a/impl/test/src/test/resources/workflows-samples/callPostHttp.yaml +++ b/impl/test/src/test/resources/workflows-samples/call-http-post.yaml @@ -1,10 +1,10 @@ document: dsl: 1.0.0-alpha1 namespace: test - name: http-call-with-response-output + name: call-http-with-response-output version: 1.0.0 do: - - postPet: + - postAuthors: call: http with: method: post diff --git a/impl/test/src/test/resources/workflows-samples/call-http-put.yaml b/impl/test/src/test/resources/workflows-samples/call-http-put.yaml new file mode 100644 index 000000000..8567e64db --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/call-http-put.yaml @@ -0,0 +1,19 @@ +document: + dsl: 1.0.0-alpha1 + namespace: test + name: call-http-put + version: 1.0.0 +do: + - updateAuthor: + call: http + with: + redirect: true + headers: + content-type: application/json + accept: application/json + method: put + endpoint: + uri: https://fakerestapi.azurewebsites.net/api/v1/Authors/1 + body: "${{firstName: .firstName}}" + output: + as: .firstName \ No newline at end of file diff --git a/impl/test/src/test/resources/workflows-samples/call-http-redirect-false.yaml b/impl/test/src/test/resources/workflows-samples/call-http-redirect-false.yaml new file mode 100644 index 000000000..05cda7e60 --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/call-http-redirect-false.yaml @@ -0,0 +1,16 @@ +document: + dsl: 1.0.0-alpha1 + namespace: test + name: call-http-redirect-false + version: 1.0.0 +do: + - postUsersWithRedirectFalse: + call: http + with: + redirect: false + headers: + content-type: application/json + method: post + endpoint: + uri: http://localhost:9876/users + body: "${{firstName: .firstName, lastName: .lastName, id: .id, bookId: .bookId}}" \ No newline at end of file diff --git a/impl/test/src/test/resources/workflows-samples/call-with-response-output-expr.yaml b/impl/test/src/test/resources/workflows-samples/call-with-response-output-expr.yaml new file mode 100644 index 000000000..57579717d --- /dev/null +++ b/impl/test/src/test/resources/workflows-samples/call-with-response-output-expr.yaml @@ -0,0 +1,18 @@ +document: + dsl: 1.0.0-alpha1 + namespace: test + name: http-call-with-response-output-expr + version: 1.0.0 +do: + - postPet: + call: http + with: + redirect: true + headers: + content-type: application/json + method: post + endpoint: + uri: http://localhost:9876/users + body: "${{firstName: .firstName, lastName: .lastName, id: .id, bookId: .bookId}}" + output: + as: .status \ No newline at end of file diff --git a/impl/test/src/test/resources/workflows-samples/openapi/project-post-positive.yaml b/impl/test/src/test/resources/workflows-samples/openapi/project-post-positive.yaml index 89f97f1b2..1c5797105 100644 --- a/impl/test/src/test/resources/workflows-samples/openapi/project-post-positive.yaml +++ b/impl/test/src/test/resources/workflows-samples/openapi/project-post-positive.yaml @@ -21,4 +21,5 @@ do: validateOnly: false notifyMembers: true lang: en - Authorization: "Bearer eyJhbnNpc2l0b3IuYm9sdXMubWFnbnVz" \ No newline at end of file + Authorization: "Bearer eyJhbnNpc2l0b3IuYm9sdXMubWFnbnVz" + redirect: true \ No newline at end of file diff --git a/impl/test/src/test/resources/workflows-samples/try-catch-retry-inline.yaml b/impl/test/src/test/resources/workflows-samples/try-catch-retry-inline.yaml index 76037fec9..69dd3f2b0 100644 --- a/impl/test/src/test/resources/workflows-samples/try-catch-retry-inline.yaml +++ b/impl/test/src/test/resources/workflows-samples/try-catch-retry-inline.yaml @@ -11,6 +11,7 @@ do: with: method: get endpoint: http://localhost:9797 + redirect: true catch: errors: with: diff --git a/impl/test/src/test/resources/workflows-samples/try-catch-retry-reusable.yaml b/impl/test/src/test/resources/workflows-samples/try-catch-retry-reusable.yaml index ecf66a46c..00834c6f0 100644 --- a/impl/test/src/test/resources/workflows-samples/try-catch-retry-reusable.yaml +++ b/impl/test/src/test/resources/workflows-samples/try-catch-retry-reusable.yaml @@ -21,6 +21,7 @@ do: with: method: get endpoint: http://localhost:9797 + redirect: true catch: errors: with: