From 6d1edab45cde0453f34460488f0c0bb246fa3443 Mon Sep 17 00:00:00 2001 From: Matheus Cruz Date: Tue, 14 Apr 2026 15:46:51 -0300 Subject: [PATCH 1/2] Introduce query methods to FuncHttp Signed-off-by: Matheus Cruz --- experimental/test/pom.xml | 37 +++ .../fluent/test/FuncHttpTest.java | 259 ++++++++++++++++++ .../fluent/spec/dsl/BaseCallHttpSpec.java | 15 + .../fluent/spec/spi/CallHttpTaskFluent.java | 15 +- .../fluent/spec/WorkflowBuilderTest.java | 15 + 5 files changed, 339 insertions(+), 2 deletions(-) create mode 100644 experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncHttpTest.java diff --git a/experimental/test/pom.xml b/experimental/test/pom.xml index 2e691e6e4..57613c57f 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,45 @@ 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 + ${project.version} + test + + + 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 000000000..3926ee330 --- /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(300, 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 ece52bcaa..bd58b3e58 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 1206b35c6..61cf0db6e 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 c98f18d13..4b3cd5813 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 @@ -613,6 +613,21 @@ void testDoTaskCallHTTPQueryConsumerAndMap() { .getHTTPQuery(); assertEquals("x", hq2.getAdditionalProperties().get("q1")); assertEquals("y", hq2.getAdditionalProperties().get("q2")); + + Workflow wf3 = + WorkflowBuilder.workflow("flowCallQuerySingle") + .tasks(d -> d.http("qryOne", http().GET().endpoint("http://uri").query("id", "42"))) + .build(); + HTTPQuery hq3 = + wf3.getDo() + .get(0) + .getTask() + .getCallTask() + .getCallHTTP() + .getWith() + .getQuery() + .getHTTPQuery(); + assertEquals("42", hq3.getAdditionalProperties().get("id")); } @Test From 2c2037a9d62b8cfd21802845c10cb88dac8442ed Mon Sep 17 00:00:00 2001 From: Matheus Cruz Date: Tue, 14 Apr 2026 16:06:02 -0300 Subject: [PATCH 2/2] Apply pull request suggestions Signed-off-by: Matheus Cruz --- experimental/test/pom.xml | 2 -- .../fluent/test/FuncHttpTest.java | 2 +- .../fluent/spec/WorkflowBuilderTest.java | 20 ++++++++----------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/experimental/test/pom.xml b/experimental/test/pom.xml index 57613c57f..5661f5822 100644 --- a/experimental/test/pom.xml +++ b/experimental/test/pom.xml @@ -71,8 +71,6 @@ io.serverlessworkflow serverlessworkflow-impl-jq - ${project.version} - test org.glassfish.jersey.media 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 index 3926ee330..724b4fa8e 100644 --- a/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncHttpTest.java +++ b/experimental/test/src/test/java/io/serverlessworkflow/fluent/test/FuncHttpTest.java @@ -54,7 +54,7 @@ void cleanup() { } private RecordedRequest takeRequestOrFail() throws Exception { - RecordedRequest request = mockServer.takeRequest(300, TimeUnit.MILLISECONDS); + RecordedRequest request = mockServer.takeRequest(150, TimeUnit.MILLISECONDS); assertThat(request) .as("Expected an HTTP request to be received by MockWebServer within 300 ms") .isNotNull(); 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 4b3cd5813..32243f28d 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( @@ -613,21 +613,17 @@ void testDoTaskCallHTTPQueryConsumerAndMap() { .getHTTPQuery(); assertEquals("x", hq2.getAdditionalProperties().get("q1")); assertEquals("y", hq2.getAdditionalProperties().get("q2")); + } - Workflow wf3 = + @Test + void testDoTaskCallHTTPQuerySingleKeyValue() { + Workflow wf = WorkflowBuilder.workflow("flowCallQuerySingle") .tasks(d -> d.http("qryOne", http().GET().endpoint("http://uri").query("id", "42"))) .build(); - HTTPQuery hq3 = - wf3.getDo() - .get(0) - .getTask() - .getCallTask() - .getCallHTTP() - .getWith() - .getQuery() - .getHTTPQuery(); - assertEquals("42", hq3.getAdditionalProperties().get("id")); + HTTPQuery hq = + wf.getDo().get(0).getTask().getCallTask().getCallHTTP().getWith().getQuery().getHTTPQuery(); + assertEquals("42", hq.getAdditionalProperties().get("id")); } @Test