This is an automated email from the ASF dual-hosted git repository.

matrei pushed a commit to branch feat/http-client-form-post
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit eb0cc8f1a68ba2abb79ea22fd420f47c7135a0ee
Author: Mattias Reichel <[email protected]>
AuthorDate: Sun Jun 14 13:26:51 2026 +0200

    feat(http-client-support): add httpPostForm method
---
 .../testing/http/client/HttpClientSupport.groovy   | 45 ++++++++++++++++++++++
 .../http/client/HttpClientSupportSpec.groovy       | 45 ++++++++++++++++++++++
 2 files changed, 90 insertions(+)

diff --git 
a/grails-testing-support-http-client/src/main/groovy/org/apache/grails/testing/http/client/HttpClientSupport.groovy
 
b/grails-testing-support-http-client/src/main/groovy/org/apache/grails/testing/http/client/HttpClientSupport.groovy
index 093940ca7c..2c59a1a6b2 100644
--- 
a/grails-testing-support-http-client/src/main/groovy/org/apache/grails/testing/http/client/HttpClientSupport.groovy
+++ 
b/grails-testing-support-http-client/src/main/groovy/org/apache/grails/testing/http/client/HttpClientSupport.groovy
@@ -21,6 +21,7 @@ package org.apache.grails.testing.http.client
 import java.net.http.HttpClient
 import java.net.http.HttpRequest
 import java.net.http.HttpResponse
+import java.lang.reflect.Array
 import java.time.Duration
 
 import groovy.json.JsonGenerator
@@ -409,6 +410,25 @@ trait HttpClientSupport {
         httpPost(headers, pathOrUrl, XmlUtils.toXml(format, body), 
APPLICATION_XML, client)
     }
 
+    /**
+     * POST form data as {@code application/x-www-form-urlencoded} (UTF-8).
+     *
+     * @param headers optional request headers
+     * @param pathOrUrl relative path or absolute URL
+     * @param formData form fields to encode into the request body; collection 
and array values are encoded as
+     * repeated keys
+     * @param client optional {@link HttpClient} instance to use
+     * @return response with String body
+     */
+    TestHttpResponse httpPostForm(
+            Map<String, String> headers = EMPTY,
+            CharSequence pathOrUrl,
+            Map<String, ?> formData,
+            HttpClient client = null
+    ) {
+        httpPost(headers, pathOrUrl, encodeFormData(formData), 
'application/x-www-form-urlencoded', client)
+    }
+
     // endregion
     // region POST MULTIPART
 
@@ -1157,6 +1177,31 @@ trait HttpClientSupport {
         builder.timeout(Duration.ofSeconds(60))
     }
 
+    private String encodeFormData(Map<String, ?> formData) {
+        formData.collectMany {
+            encodeFormValues(it.key, it.value)
+        }.join('&')
+    }
+
+    private List<String> encodeFormValues(String key, Object value) {
+        if (value == null) {
+            value = ''
+        }
+        if (value instanceof Iterable) {
+            return value.collect { encodeFormEntry(key, it) }
+        }
+        if (value.class.array) {
+            return (0..<Array.getLength(value)).collect {
+                encodeFormEntry(key, Array.get(value, it))
+            }
+        }
+        [encodeFormEntry(key, value)]
+    }
+
+    private String encodeFormEntry(String key, Object value) {
+        "${URLEncoder.encode(key, 
'UTF-8')}=${URLEncoder.encode(value?.toString() ?: '', 'UTF-8')}"
+    }
+
     private void warn(String[] lines) {
         println('*** WARNING ***')
         lines.each {
diff --git 
a/grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/HttpClientSupportSpec.groovy
 
b/grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/HttpClientSupportSpec.groovy
index 92afb39b0c..b2794ffe1c 100644
--- 
a/grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/HttpClientSupportSpec.groovy
+++ 
b/grails-testing-support-http-client/src/test/groovy/org/apache/grails/testing/http/client/HttpClientSupportSpec.groovy
@@ -408,6 +408,51 @@ class HttpClientSupportSpec extends Specification {
         'custom' | 
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build()
     }
 
+    void 'form request helper encodes multi-value data and forwards headers'() 
{
+        given:
+        server.expectations {
+            POST('/form/post-default') {
+                called(1)
+                decoder('application/x-www-form-urlencoded', 
Decoders.utf8String)
+                
body('name=Post+Default&tag=red&tag=blue&description=R%26D%2FQA', 
'application/x-www-form-urlencoded')
+                responder {
+                    body('hello')
+                }
+            }
+            POST('/form/post-header-client') {
+                called(1)
+                header('X-Req', 'F1')
+                decoder('application/x-www-form-urlencoded', 
Decoders.utf8String)
+                body('name=Post+Header+Client&description=R%26D%2FQA', 
'application/x-www-form-urlencoded')
+                responder {
+                    body('hello')
+                }
+            }
+        }
+
+        and:
+        def testClient = new TestClient(server.httpUrl)
+        def customClient = 
HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(2)).build()
+        def multiValueFormData = [name: 'Post Default', tag: ['red', 'blue'], 
description: 'R&D/QA']
+
+        when:
+        def postResponse = testClient.httpPostForm(
+                '/form/post-default',
+                multiValueFormData
+        )
+        def postHeaderResponse = testClient.httpPostForm(
+                'X-Req': 'F1',
+                '/form/post-header-client',
+                [name: 'Post Header Client', description: 'R&D/QA'],
+                customClient
+        )
+
+        then:
+        postResponse.assertEquals(200, 'hello')
+        postHeaderResponse.assertEquals(200, 'hello')
+        server.verify()
+    }
+
     @Unroll
     void 'post with MultipartBody sends multipart content type and payload 
bytes with #label client'(HttpClient client) {
         given:

Reply via email to