diff --git a/experimental/test/pom.xml b/experimental/test/pom.xml index 2e691e6e..5661f582 100644 --- a/experimental/test/pom.xml +++ b/experimental/test/pom.xml @@ -7,6 +7,9 @@ serverlessworkflow-experimental-test Serverless Workflow :: Experimental :: Test + + 4.0.2 + org.junit.jupiter @@ -43,11 +46,43 @@ serverlessworkflow-experimental-lambda test + + io.serverlessworkflow + serverlessworkflow-impl-http + ${project.version} + test + io.serverlessworkflow serverlessworkflow-impl-model ${project.version} test + + com.squareup.okhttp3 + mockwebserver + + + io.serverlessworkflow + serverlessworkflow-impl-template-resolver + ${project.version} + test + + + io.serverlessworkflow + serverlessworkflow-impl-jq + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${version.org.glassfish.jersey} + test + + + org.glassfish.jersey.core + jersey-client + ${version.org.glassfish.jersey} + test + \ No newline at end of file diff --git a/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncHttpTest.java b/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncHttpTest.java new file mode 100644 index 00000000..724b4fa8 --- /dev/null +++ b/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncHttpTest.java @@ -0,0 +1,259 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.fluent.test; + +import static io.serverlessworkflow.fluent.func.dsl.FuncDSL.http; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.SoftAssertions.assertSoftly; + +import io.serverlessworkflow.fluent.func.FuncWorkflowBuilder; +import io.serverlessworkflow.impl.WorkflowApplication; +import io.serverlessworkflow.impl.WorkflowInstance; +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import mockwebserver3.MockResponse; +import mockwebserver3.MockWebServer; +import mockwebserver3.RecordedRequest; +import okhttp3.Headers; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class FuncHttpTest { + + private WorkflowApplication app; + private MockWebServer mockServer; + + @BeforeEach + void setup() throws IOException { + app = WorkflowApplication.builder().build(); + mockServer = new MockWebServer(); + mockServer.start(0); + mockServer.enqueue(new MockResponse(204, Headers.of("Content-Type", "application/json"), "")); + } + + @AfterEach + void cleanup() { + mockServer.close(); + app.close(); + } + + private RecordedRequest takeRequestOrFail() throws Exception { + RecordedRequest request = mockServer.takeRequest(150, TimeUnit.MILLISECONDS); + assertThat(request) + .as("Expected an HTTP request to be received by MockWebServer within 300 ms") + .isNotNull(); + return request; + } + + @Test + @DisplayName("Query method with single key-value pair") + void test_query_with_single_key_value() throws Exception { + var workflow = + FuncWorkflowBuilder.workflow("test-query-single") + .tasks( + http("callHttp") + .GET() + .uri(mockServer.url("/api/endpoint").toString()) + .query("param1", "value1")) + .build(); + + WorkflowInstance instance = app.workflowDefinition(workflow).instance(Map.of()); + + instance.start().join(); + + RecordedRequest request = takeRequestOrFail(); + + assertSoftly( + softly -> { + softly.assertThat(request.getUrl().toString()).contains("param1=value1"); + softly.assertThat(request.getMethod()).isEqualTo("GET"); + }); + } + + @Test + @DisplayName("Query method with multiple single key-value pairs (individually tested)") + void test_query_with_multiple_single_values() throws Exception { + var workflow = + FuncWorkflowBuilder.workflow("test-query-single-multi") + .tasks( + http("callHttp") + .GET() + .uri(mockServer.url("/api/endpoint").toString()) + .query("param1", "value1") + .query("param2", "value2") + .query("param3", "value3")) + .build(); + + WorkflowInstance instance = app.workflowDefinition(workflow).instance(Map.of()); + instance.start().join(); + + RecordedRequest request = takeRequestOrFail(); + String url = request.getUrl().toString(); + + assertSoftly( + softly -> { + softly.assertThat(url).contains("param1=value1").isNotEmpty(); + softly.assertThat(url).contains("param2=value2").isNotEmpty(); + softly.assertThat(url).contains("param3=value3").isNotEmpty(); + }); + } + + @Test + @DisplayName("Query method with Map of parameters") + void test_query_with_map() throws Exception { + + var workflow = + FuncWorkflowBuilder.workflow("test-query-map") + .tasks( + http("callHttp") + .GET() + .uri(mockServer.url("/api/endpoint").toString()) + .query(Map.of("userId", "123", "userName", "john", "status", "active"))) + .build(); + + WorkflowInstance instance = app.workflowDefinition(workflow).instance(Map.of()); + instance.start().join(); + + RecordedRequest request = takeRequestOrFail(); + String url = request.getUrl().toString(); + + assertSoftly( + softly -> { + softly.assertThat(url).contains("userId=123"); + softly.assertThat(url).contains("userName=john"); + softly.assertThat(url).contains("status=active"); + }); + } + + @Test + @DisplayName("Query method with expression string") + void test_query_with_expression() throws Exception { + var workflow = + FuncWorkflowBuilder.workflow("test-query-expression") + .tasks( + http("callHttp") + .GET() + .uri(mockServer.url("/api/endpoint").toString()) + .query("enabled", "${ .enabled }")) + .build(); + + WorkflowInstance instance = app.workflowDefinition(workflow).instance(Map.of("enabled", true)); + instance.start().join(); + + RecordedRequest request = takeRequestOrFail(); + + assertThat(request.getUrl().query()).contains("enabled=true"); + } + + @Test + @DisplayName("Query method with empty Map") + void test_query_with_empty_map() throws Exception { + var workflow = + FuncWorkflowBuilder.workflow("test-query-empty-map") + .tasks( + http("callHttp") + .GET() + .uri(mockServer.url("/api/endpoint").toString()) + .query(Map.of())) + .build(); + + WorkflowInstance instance = app.workflowDefinition(workflow).instance(Map.of()); + instance.start().join(); + + RecordedRequest request = takeRequestOrFail(); + + assertSoftly( + softly -> { + softly.assertThat(request.getUrl().encodedPath()).isEqualTo("/api/endpoint"); + softly.assertThat(request.getUrl().encodedQuery()).isNull(); + }); + } + + @Test + @DisplayName("Query method with special characters in values") + void test_query_with_special_characters() throws Exception { + var workflow = + FuncWorkflowBuilder.workflow("test-query-special-chars") + .tasks( + http("callHttp") + .GET() + .uri(mockServer.url("/api/endpoint").toString()) + .query("email", "user@example.com")) + .build(); + + WorkflowInstance instance = app.workflowDefinition(workflow).instance(Map.of()); + instance.start().join(); + + RecordedRequest request = takeRequestOrFail(); + + assertSoftly( + softly -> { + softly.assertThat(request.getUrl().queryParameter("email")).isEqualTo("user@example.com"); + softly.assertThat(request.getUrl().encodedQuery()).contains("email=user%40example.com"); + }); + } + + @Test + @DisplayName("Query method overload - Map with multiple values") + void test_query_map_multiple_values() throws Exception { + var workflow = + FuncWorkflowBuilder.workflow("test-query-map-multi") + .tasks( + http("callHttp") + .GET() + .uri(mockServer.url("/api/endpoint").toString()) + .query(Map.of("limit", "50", "offset", "0", "sort", "name"))) + .build(); + + WorkflowInstance instance = app.workflowDefinition(workflow).instance(Map.of()); + instance.start().join(); + + RecordedRequest request = takeRequestOrFail(); + String url = request.getUrl().toString(); + + assertSoftly( + softly -> { + softly.assertThat(url).contains("limit=50"); + softly.assertThat(url).contains("offset=0"); + softly.assertThat(url).contains("sort=name"); + }); + } + + @Test + @DisplayName("Query method with headers and query parameters") + void test_query_with_headers_and_query() throws Exception { + var workflow = + FuncWorkflowBuilder.workflow("test-query-with-headers") + .tasks( + http("callHttp") + .GET() + .uri(mockServer.url("/api/endpoint").toString()) + .header("Authorization", "Bearer token123") + .header("Accept", "application/json") + .query("userId", "123")) + .build(); + + WorkflowInstance instance = app.workflowDefinition(workflow).instance(Map.of()); + instance.start().join(); + + RecordedRequest request = takeRequestOrFail(); + String url = request.getUrl().toString(); + assertThat(url).contains("userId=123"); + } +} diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseCallHttpSpec.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseCallHttpSpec.java index ece52bca..bd58b3e5 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseCallHttpSpec.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/dsl/BaseCallHttpSpec.java @@ -107,6 +107,21 @@ default SELF headers(Map headers) { return self(); } + default SELF query(String queryExpr) { + steps().add(c -> c.query(queryExpr)); + return self(); + } + + default SELF query(Map query) { + steps().add(c -> c.query(query)); + return self(); + } + + default SELF query(String name, String value) { + steps().add(c -> c.query(q -> q.query(name, value))); + return self(); + } + default void accept(CallHttpTaskFluent b) { for (var s : steps()) { s.accept(b); diff --git a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallHttpTaskFluent.java b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallHttpTaskFluent.java index 1206b35c..61cf0db6 100644 --- a/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallHttpTaskFluent.java +++ b/fluent/spec/src/main/java/io/serverlessworkflow/fluent/spec/spi/CallHttpTaskFluent.java @@ -148,19 +148,30 @@ default SELF query(String expr) { } default SELF query(Consumer consumer) { - HTTPQueryBuilder queryBuilder = new HTTPQueryBuilder(); + HTTPQueryBuilder queryBuilder = createHttpQueryFromExisting(); consumer.accept(queryBuilder); ((CallHTTP) this.self().getTask()).getWith().setQuery(queryBuilder.build()); return self(); } default SELF query(Map query) { - HTTPQueryBuilder httpQueryBuilder = new HTTPQueryBuilder(); + HTTPQueryBuilder httpQueryBuilder = createHttpQueryFromExisting(); httpQueryBuilder.queries(query); ((CallHTTP) this.self().getTask()).getWith().setQuery(httpQueryBuilder.build()); return self(); } + private HTTPQueryBuilder createHttpQueryFromExisting() { + HTTPQueryBuilder httpQueryBuilder = new HTTPQueryBuilder(); + Query existingQuery = ((CallHTTP) this.self().getTask()).getWith().getQuery(); + if (existingQuery != null + && existingQuery.getHTTPQuery() != null + && existingQuery.getHTTPQuery().getAdditionalProperties() != null) { + existingQuery.getHTTPQuery().getAdditionalProperties().forEach(httpQueryBuilder::query); + } + return httpQueryBuilder; + } + default SELF redirect(boolean redirect) { ((CallHTTP) this.self().getTask()).getWith().setRedirect(redirect); return self(); diff --git a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java index c98f18d1..32243f28 100644 --- a/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java +++ b/fluent/spec/src/test/java/io/serverlessworkflow/fluent/spec/WorkflowBuilderTest.java @@ -575,7 +575,7 @@ void testDoTaskCallHTTPHeadersConsumerAndMap() { } @Test - void testDoTaskCallHTTPQueryConsumerAndMap() { + void testDoTaskCallHTTPQueryMap() { Workflow wf = WorkflowBuilder.workflow("flowCallQuery") .tasks( @@ -615,6 +615,17 @@ void testDoTaskCallHTTPQueryConsumerAndMap() { assertEquals("y", hq2.getAdditionalProperties().get("q2")); } + @Test + void testDoTaskCallHTTPQuerySingleKeyValue() { + Workflow wf = + WorkflowBuilder.workflow("flowCallQuerySingle") + .tasks(d -> d.http("qryOne", http().GET().endpoint("http://uri").query("id", "42"))) + .build(); + HTTPQuery hq = + wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith().getQuery().getHTTPQuery(); + assertEquals("42", hq.getAdditionalProperties().get("id")); + } + @Test void testDoTaskCallHTTPRedirectAndOutput() { Workflow wf =