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

jamesfredley pushed a commit to branch test/expand-integration-test-coverage
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 61b87c1a15c9e7e7bba44d9683612a462d4fbda1
Author: James Fredley <[email protected]>
AuthorDate: Sun Jan 25 22:06:39 2026 -0500

    Add HTTP error handling, request/response, CORS, and file upload tests
    
    - Add ErrorHandlingSpec with 23 tests for HTTP error responses
    - Tests all common HTTP status codes (400-503)
    - Tests JSON error payloads and custom error headers
    - Add RequestResponseSpec with 20 tests for HTTP handling
    - Tests headers, cookies, sessions, request info
    - Add CorsAdvancedSpec with 16 tests for CORS headers
    - Tests preflight requests and cross-origin scenarios
    - Add FileUploadSpec with 15 tests for multipart uploads
    - Tests single/multiple files, validation, content processing
---
 .../controllers/functionaltests/UrlMappings.groovy |  59 +++
 .../functionaltests/cors/CorsTestController.groovy | 113 ++++++
 .../ErrorHandlingTestController.groovy             | 215 +++++++++++
 .../fileupload/FileUploadTestController.groovy     | 241 +++++++++++++
 .../RequestResponseTestController.groovy           | 270 ++++++++++++++
 .../functionaltests/cors/CorsAdvancedSpec.groovy   | 301 ++++++++++++++++
 .../errorhandling/ErrorHandlingSpec.groovy         | 363 +++++++++++++++++++
 .../fileupload/FileUploadSpec.groovy               | 395 +++++++++++++++++++++
 .../requestresponse/RequestResponseSpec.groovy     | 388 ++++++++++++++++++++
 9 files changed, 2345 insertions(+)

diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/UrlMappings.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/UrlMappings.groovy
index 72757b478b..85ca8e1e7b 100644
--- 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/UrlMappings.groovy
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/UrlMappings.groovy
@@ -43,6 +43,65 @@ class UrlMappings {
 
         "/forward/$param1"(controller: 'forwarding', action: 'two')
 
+        // === URL Mappings Test Routes ===
+        
+        // Static path mapping
+        "/api/test"(controller: 'urlMappingsTest', action: 'index')
+        
+        // Path variable mapping
+        "/api/items/$id"(controller: 'urlMappingsTest', action: 'show')
+        
+        // Multiple path variables (date pattern)
+        "/api/archive/$year/$month/$day"(controller: 'urlMappingsTest', 
action: 'pathVars')
+        
+        // Named URL mapping
+        name testNamed: "/api/named/$name"(controller: 'urlMappingsTest', 
action: 'named')
+        
+        // Constrained path variable (only uppercase letters allowed)
+        "/api/codes/$code" {
+            controller = 'urlMappingsTest'
+            action = 'constrained'
+            constraints {
+                code matches: /[A-Z]+/
+            }
+        }
+        
+        // Wildcard double-star captures remaining path
+        "/api/files/**"(controller: 'urlMappingsTest', action: 'wildcard') {
+            path = { request.forwardURI - '/api/files/' }
+        }
+        
+        // HTTP method constraints
+        "/api/resources"(controller: 'urlMappingsTest') {
+            action = [GET: 'list', POST: 'save']
+        }
+        "/api/resources/$id"(controller: 'urlMappingsTest') {
+            action = [GET: 'show', PUT: 'update', DELETE: 'delete']
+        }
+        
+        // Optional path variable
+        "/api/optional/$required/$optional?"(controller: 'urlMappingsTest', 
action: 'optional')
+        
+        // HTTP method only mapping
+        "/api/method-test"(controller: 'urlMappingsTest', action: 'httpMethod')
+        
+        // Redirect mapping with permanent flag
+        "/api/old-endpoint"(redirect: '/api/test', permanent: true)
+
+        // === CORS Test Routes (under /api/** which has CORS enabled) ===
+        "/api/cors"(controller: 'corsTest', action: 'index')
+        "/api/cors/data"(controller: 'corsTest', action: 'getData')
+        "/api/cors/items/$id"(controller: 'corsTest') {
+            action = [GET: 'getItem', PUT: 'update', DELETE: 'delete']
+        }
+        "/api/cors/items"(controller: 'corsTest') {
+            action = [GET: 'getData', POST: 'create']
+        }
+        "/api/cors/custom-headers"(controller: 'corsTest', action: 
'withCustomHeaders')
+        "/api/cors/echo-origin"(controller: 'corsTest', action: 'echoOrigin')
+        "/api/cors/authenticated"(controller: 'corsTest', action: 
'authenticated')
+        "/api/cors/slow"(controller: 'corsTest', action: 'slowRequest')
+
         "/"(view:"/index")
         "500"(view:'/error')
         "404"(controller:"errors", action:"notFound")
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/cors/CorsTestController.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/cors/CorsTestController.groovy
new file mode 100644
index 0000000000..d0c4055fc8
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/cors/CorsTestController.groovy
@@ -0,0 +1,113 @@
+/*
+ *  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
+ *
+ *    https://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 functionaltests.cors
+
+import grails.converters.JSON
+
+/**
+ * Controller for testing CORS (Cross-Origin Resource Sharing) functionality.
+ * This controller provides endpoints under /api/* which have CORS enabled
+ * via application.yml configuration.
+ */
+class CorsTestController {
+
+    static responseFormats = ['json']
+
+    // ========== Basic CORS Endpoints ==========
+
+    def index() {
+        render([message: 'CORS test endpoint', path: '/api/corsTest'] as JSON)
+    }
+
+    def getData() {
+        render([
+            data: [
+                [id: 1, name: 'Item 1', description: 'First item'],
+                [id: 2, name: 'Item 2', description: 'Second item'],
+                [id: 3, name: 'Item 3', description: 'Third item']
+            ],
+            total: 3
+        ] as JSON)
+    }
+
+    def getItem() {
+        def id = params.id
+        render([id: id, name: "Item ${id}", retrieved: true] as JSON)
+    }
+
+    // ========== POST/PUT/DELETE endpoints for CORS testing ==========
+
+    def create() {
+        def data = request.JSON ?: [:]
+        render([created: true, data: data, method: 'POST'] as JSON)
+    }
+
+    def update() {
+        def id = params.id
+        def data = request.JSON ?: [:]
+        render([updated: true, id: id, data: data, method: 'PUT'] as JSON)
+    }
+
+    def delete() {
+        def id = params.id
+        render([deleted: true, id: id, method: 'DELETE'] as JSON)
+    }
+
+    // ========== Custom Header Endpoints ==========
+
+    def withCustomHeaders() {
+        response.setHeader('X-Custom-Response', 'custom-value')
+        response.setHeader('X-Request-Timestamp', 
String.valueOf(System.currentTimeMillis()))
+        render([
+            message: 'Response with custom headers',
+            customHeadersSet: true
+        ] as JSON)
+    }
+
+    def echoOrigin() {
+        def origin = request.getHeader('Origin')
+        render([
+            receivedOrigin: origin,
+            message: 'Origin header received'
+        ] as JSON)
+    }
+
+    // ========== Authenticated/Credentials Endpoint ==========
+
+    def authenticated() {
+        def authHeader = request.getHeader('Authorization')
+        def hasCredentials = authHeader != null
+        render([
+            authenticated: hasCredentials,
+            authType: hasCredentials ? authHeader.split(' ')[0] : null,
+            message: hasCredentials ? 'Credentials received' : 'No credentials'
+        ] as JSON)
+    }
+
+    // ========== Long-running request for timing ==========
+
+    def slowRequest() {
+        Thread.sleep(100) // Simulate processing
+        render([
+            completed: true,
+            processingTime: 100,
+            message: 'Slow request completed'
+        ] as JSON)
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/errorhandling/ErrorHandlingTestController.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/errorhandling/ErrorHandlingTestController.groovy
new file mode 100644
index 0000000000..0b7ae3e3fc
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/errorhandling/ErrorHandlingTestController.groovy
@@ -0,0 +1,215 @@
+/*
+ *  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
+ *
+ *    https://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 functionaltests.errorhandling
+
+import grails.converters.JSON
+import grails.web.http.HttpHeaders
+
+/**
+ * Controller demonstrating various error handling patterns in Grails.
+ */
+class ErrorHandlingTestController {
+
+    static responseFormats = ['json', 'html']
+
+    // ========== HTTP Status Code Tests ==========
+
+    def renderNotFound() {
+        render status: 404, text: 'Resource not found'
+    }
+
+    def renderBadRequest() {
+        render status: 400, text: 'Bad request'
+    }
+
+    def renderUnauthorized() {
+        render status: 401, text: 'Unauthorized'
+    }
+
+    def renderForbidden() {
+        render status: 403, text: 'Forbidden'
+    }
+
+    def renderMethodNotAllowed() {
+        render status: 405, text: 'Method not allowed'
+    }
+
+    def renderConflict() {
+        render status: 409, text: 'Conflict'
+    }
+
+    def renderGone() {
+        render status: 410, text: 'Gone'
+    }
+
+    def renderUnprocessableEntity() {
+        render status: 422, text: 'Unprocessable entity'
+    }
+
+    def renderTooManyRequests() {
+        render status: 429, text: 'Too many requests'
+    }
+
+    def renderInternalServerError() {
+        render status: 500, text: 'Internal server error'
+    }
+
+    def renderServiceUnavailable() {
+        render status: 503, text: 'Service unavailable'
+    }
+
+    // ========== JSON Error Response Tests ==========
+
+    def jsonNotFound() {
+        response.status = 404
+        render([error: 'not_found', message: 'The requested resource was not 
found'] as JSON)
+    }
+
+    def jsonBadRequest() {
+        response.status = 400
+        render([error: 'bad_request', message: 'Invalid request parameters', 
details: [field: 'name', issue: 'required']] as JSON)
+    }
+
+    def jsonValidationError() {
+        response.status = 422
+        render([
+            error: 'validation_error',
+            message: 'Validation failed',
+            errors: [
+                [field: 'email', message: 'Invalid email format'],
+                [field: 'age', message: 'Must be at least 18']
+            ]
+        ] as JSON)
+    }
+
+    def jsonServerError() {
+        response.status = 500
+        render([error: 'internal_error', message: 'An unexpected error 
occurred', requestId: UUID.randomUUID().toString()] as JSON)
+    }
+
+    // ========== Exception Throwing Tests ==========
+
+    def throwRuntimeException() {
+        throw new RuntimeException('A runtime error occurred')
+    }
+
+    def throwIllegalArgumentException() {
+        throw new IllegalArgumentException('Invalid argument provided')
+    }
+
+    def throwIllegalStateException() {
+        throw new IllegalStateException('Invalid state')
+    }
+
+    def throwNullPointerException() {
+        String s = null
+        s.length() // This will throw NPE
+    }
+
+    def throwIndexOutOfBounds() {
+        def list = []
+        list[10] // This will throw IndexOutOfBoundsException
+    }
+
+    def throwArithmeticException() {
+        def result = 1 / 0 // This will throw ArithmeticException
+        render([result: result] as JSON)
+    }
+
+    def throwNumberFormatException() {
+        Integer.parseInt('not-a-number')
+    }
+
+    def throwCustomBusinessException() {
+        throw new BusinessException('INVALID_ORDER', 'Order cannot be 
processed')
+    }
+
+    def throwNestedExceptions() {
+        try {
+            throw new IllegalArgumentException('Root cause')
+        } catch (Exception e) {
+            throw new RuntimeException('Wrapper exception', e)
+        }
+    }
+
+    // ========== Conditional Error Handling ==========
+
+    def conditionalError() {
+        def condition = params.condition
+        switch (condition) {
+            case 'notfound':
+                response.status = 404
+                render([error: 'not_found'] as JSON)
+                break
+            case 'badrequest':
+                response.status = 400
+                render([error: 'bad_request'] as JSON)
+                break
+            case 'forbidden':
+                response.status = 403
+                render([error: 'forbidden'] as JSON)
+                break
+            case 'error':
+                throw new RuntimeException('Conditional error triggered')
+            default:
+                render([status: 'ok', condition: condition] as JSON)
+        }
+    }
+
+    // ========== Response with Headers ==========
+
+    def errorWithHeaders() {
+        response.status = 429
+        response.setHeader('Retry-After', '60')
+        response.setHeader('X-RateLimit-Limit', '100')
+        response.setHeader('X-RateLimit-Remaining', '0')
+        response.setHeader('X-RateLimit-Reset', 
String.valueOf(System.currentTimeMillis() + 60000))
+        render([error: 'rate_limited', retryAfter: 60] as JSON)
+    }
+
+    def notFoundWithHints() {
+        response.status = 404
+        response.setHeader('X-Suggested-Resource', '/api/items')
+        render([error: 'not_found', suggestions: ['/api/items', 
'/api/products']] as JSON)
+    }
+
+    // ========== Successful Operations for Comparison ==========
+
+    def success() {
+        render([status: 'ok', message: 'Operation successful'] as JSON)
+    }
+
+    def successWithData() {
+        render([status: 'ok', data: [id: 1, name: 'Test Item', createdAt: new 
Date().format("yyyy-MM-dd'T'HH:mm:ss'Z'")]] as JSON)
+    }
+}
+
+/**
+ * Custom business exception for testing exception handling.
+ */
+class BusinessException extends RuntimeException {
+    String code
+    String description
+
+    BusinessException(String code, String description) {
+        super("$code: $description")
+        this.code = code
+        this.description = description
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/fileupload/FileUploadTestController.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/fileupload/FileUploadTestController.groovy
new file mode 100644
index 0000000000..c67d7318f3
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/fileupload/FileUploadTestController.groovy
@@ -0,0 +1,241 @@
+/*
+ *  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
+ *
+ *    https://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 functionaltests.fileupload
+
+import grails.converters.JSON
+import org.springframework.web.multipart.MultipartFile
+
+/**
+ * Controller for testing file upload functionality in Grails.
+ * Tests various file upload patterns including single file, multiple files,
+ * file validation, and metadata extraction.
+ */
+class FileUploadTestController {
+
+    static responseFormats = ['json', 'html']
+
+    // ========== Single File Upload ==========
+
+    def uploadSingle() {
+        def file = request.getFile('file')
+        if (!file || file.empty) {
+            response.status = 400
+            render([error: 'no_file', message: 'No file uploaded'] as JSON)
+            return
+        }
+
+        render([
+            success: true,
+            filename: file.originalFilename,
+            size: file.size,
+            contentType: file.contentType
+        ] as JSON)
+    }
+
+    def uploadWithMetadata() {
+        def file = request.getFile('file')
+        def description = params.description
+        def category = params.category
+
+        if (!file || file.empty) {
+            response.status = 400
+            render([error: 'no_file', message: 'No file uploaded'] as JSON)
+            return
+        }
+
+        render([
+            success: true,
+            filename: file.originalFilename,
+            size: file.size,
+            contentType: file.contentType,
+            description: description,
+            category: category
+        ] as JSON)
+    }
+
+    // ========== Multiple File Upload ==========
+
+    def uploadMultiple() {
+        def files = request.getFiles('files')
+        if (!files || files.every { it.empty }) {
+            response.status = 400
+            render([error: 'no_files', message: 'No files uploaded'] as JSON)
+            return
+        }
+
+        def uploadedFiles = files.findAll { !it.empty }.collect { file ->
+            [
+                filename: file.originalFilename,
+                size: file.size,
+                contentType: file.contentType
+            ]
+        }
+
+        render([
+            success: true,
+            count: uploadedFiles.size(),
+            files: uploadedFiles
+        ] as JSON)
+    }
+
+    // ========== File Content Processing ==========
+
+    def uploadTextFile() {
+        def file = request.getFile('file')
+        if (!file || file.empty) {
+            response.status = 400
+            render([error: 'no_file', message: 'No file uploaded'] as JSON)
+            return
+        }
+
+        def content = new String(file.bytes, 'UTF-8')
+        def lineCount = content.count('\n') + 1
+        def wordCount = content.split(/\s+/).length
+
+        render([
+            success: true,
+            filename: file.originalFilename,
+            size: file.size,
+            lineCount: lineCount,
+            wordCount: wordCount,
+            preview: content.take(100)
+        ] as JSON)
+    }
+
+    def uploadAndEcho() {
+        def file = request.getFile('file')
+        if (!file || file.empty) {
+            response.status = 400
+            render([error: 'no_file', message: 'No file uploaded'] as JSON)
+            return
+        }
+
+        def content = new String(file.bytes, 'UTF-8')
+        render([
+            success: true,
+            filename: file.originalFilename,
+            content: content
+        ] as JSON)
+    }
+
+    // ========== File Validation ==========
+
+    def uploadWithValidation() {
+        def file = request.getFile('file')
+        if (!file || file.empty) {
+            response.status = 400
+            render([error: 'no_file', message: 'No file uploaded'] as JSON)
+            return
+        }
+
+        // Size validation (max 10KB for this test)
+        def maxSize = 10 * 1024
+        if (file.size > maxSize) {
+            response.status = 400
+            render([error: 'file_too_large', message: "File exceeds max size 
of ${maxSize} bytes", actualSize: file.size] as JSON)
+            return
+        }
+
+        // Type validation - normalize content type (remove charset if present)
+        def contentType = file.contentType?.split(';')?.first()?.trim()
+        def allowedTypes = ['text/plain', 'application/json', 'text/csv']
+        if (!allowedTypes.contains(contentType)) {
+            response.status = 400
+            render([error: 'invalid_type', message: "File type 
${file.contentType} not allowed", allowedTypes: allowedTypes] as JSON)
+            return
+        }
+
+        render([
+            success: true,
+            validated: true,
+            filename: file.originalFilename,
+            size: file.size,
+            contentType: file.contentType
+        ] as JSON)
+    }
+
+    def uploadWithExtensionValidation() {
+        def file = request.getFile('file')
+        if (!file || file.empty) {
+            response.status = 400
+            render([error: 'no_file', message: 'No file uploaded'] as JSON)
+            return
+        }
+
+        def allowedExtensions = ['txt', 'csv', 'json', 'xml']
+        def filename = file.originalFilename
+        def extension = filename.contains('.') ? 
filename.substring(filename.lastIndexOf('.') + 1).toLowerCase() : ''
+
+        if (!allowedExtensions.contains(extension)) {
+            response.status = 400
+            render([error: 'invalid_extension', message: "Extension 
'${extension}' not allowed", allowedExtensions: allowedExtensions] as JSON)
+            return
+        }
+
+        render([
+            success: true,
+            filename: filename,
+            extension: extension,
+            validated: true
+        ] as JSON)
+    }
+
+    // ========== File Info Extraction ==========
+
+    def getFileInfo() {
+        def file = request.getFile('file')
+        if (!file || file.empty) {
+            response.status = 400
+            render([error: 'no_file', message: 'No file uploaded'] as JSON)
+            return
+        }
+
+        def filename = file.originalFilename
+        def extension = filename.contains('.') ? 
filename.substring(filename.lastIndexOf('.') + 1).toLowerCase() : ''
+        def basename = filename.contains('.') ? filename.substring(0, 
filename.lastIndexOf('.')) : filename
+
+        render([
+            originalFilename: filename,
+            basename: basename,
+            extension: extension,
+            size: file.size,
+            sizeKB: (file.size / 1024).round(2),
+            contentType: file.contentType,
+            isEmpty: file.empty
+        ] as JSON)
+    }
+
+    // ========== Params-based Access ==========
+
+    def uploadViaParams() {
+        def file = params.file
+        if (!file || !(file instanceof MultipartFile) || file.empty) {
+            response.status = 400
+            render([error: 'no_file', message: 'No file in params'] as JSON)
+            return
+        }
+
+        render([
+            success: true,
+            accessedViaParams: true,
+            filename: file.originalFilename,
+            size: file.size
+        ] as JSON)
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/requestresponse/RequestResponseTestController.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/requestresponse/RequestResponseTestController.groovy
new file mode 100644
index 0000000000..2ccbd247e4
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/requestresponse/RequestResponseTestController.groovy
@@ -0,0 +1,270 @@
+/*
+ *  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
+ *
+ *    https://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 functionaltests.requestresponse
+
+import grails.converters.JSON
+
+/**
+ * Controller demonstrating request/response handling patterns in Grails,
+ * including headers, cookies, session, and request attributes.
+ */
+class RequestResponseTestController {
+
+    static responseFormats = ['json', 'html']
+
+    // ========== Request Header Tests ==========
+
+    def echoHeaders() {
+        def headers = [:]
+        request.headerNames.each { name ->
+            headers[name] = request.getHeader(name)
+        }
+        render([headers: headers] as JSON)
+    }
+
+    def getSpecificHeader() {
+        def headerName = params.headerName ?: 'X-Custom-Header'
+        def headerValue = request.getHeader(headerName)
+        render([headerName: headerName, headerValue: headerValue] as JSON)
+    }
+
+    def checkUserAgent() {
+        def userAgent = request.getHeader('User-Agent')
+        def isBrowser = userAgent?.contains('Mozilla') || 
userAgent?.contains('Chrome')
+        render([userAgent: userAgent, isBrowser: isBrowser] as JSON)
+    }
+
+    def checkAcceptHeader() {
+        def accept = request.getHeader('Accept')
+        def acceptsJson = accept?.contains('application/json')
+        def acceptsHtml = accept?.contains('text/html')
+        def acceptsAll = accept?.contains('*/*')
+        render([accept: accept, acceptsJson: acceptsJson, acceptsHtml: 
acceptsHtml, acceptsAll: acceptsAll] as JSON)
+    }
+
+    def checkContentType() {
+        def contentType = request.contentType
+        render([contentType: contentType] as JSON)
+    }
+
+    // ========== Response Header Tests ==========
+
+    def setCustomHeaders() {
+        response.setHeader('X-Custom-Header', 'CustomValue')
+        response.setHeader('X-Request-Id', UUID.randomUUID().toString())
+        response.setHeader('X-Timestamp', 
String.valueOf(System.currentTimeMillis()))
+        render([status: 'ok', message: 'Headers set'] as JSON)
+    }
+
+    def setCacheHeaders() {
+        response.setHeader('Cache-Control', 'max-age=3600, public')
+        response.setHeader('ETag', '"abc123"')
+        response.setHeader('Last-Modified', 'Wed, 21 Oct 2025 07:28:00 GMT')
+        render([status: 'ok', cached: true] as JSON)
+    }
+
+    def setNoCacheHeaders() {
+        response.setHeader('Cache-Control', 'no-cache, no-store, 
must-revalidate')
+        response.setHeader('Pragma', 'no-cache')
+        response.setHeader('Expires', '0')
+        render([status: 'ok', cached: false] as JSON)
+    }
+
+    def setContentDisposition() {
+        response.setHeader('Content-Disposition', 'attachment; 
filename="report.pdf"')
+        response.contentType = 'application/pdf'
+        render([status: 'ok', downloadable: true] as JSON)
+    }
+
+    def setMultipleCustomHeaders() {
+        5.times { i ->
+            response.setHeader("X-Custom-${i}", "Value-${i}")
+        }
+        render([status: 'ok', headersSet: 5] as JSON)
+    }
+
+    // ========== Cookie Tests ==========
+
+    def setCookie() {
+        def cookieName = params.name ?: 'testCookie'
+        def cookieValue = params.value ?: 'testValue'
+        def maxAge = params.int('maxAge') ?: 3600
+        
+        def cookie = new jakarta.servlet.http.Cookie(cookieName, cookieValue)
+        cookie.maxAge = maxAge
+        cookie.path = '/'
+        response.addCookie(cookie)
+        
+        render([status: 'ok', cookieSet: true, name: cookieName, value: 
cookieValue, maxAge: maxAge] as JSON)
+    }
+
+    def setSecureCookie() {
+        def cookie = new jakarta.servlet.http.Cookie('secureCookie', 
'secureValue')
+        cookie.maxAge = 3600
+        cookie.path = '/'
+        cookie.secure = true
+        cookie.httpOnly = true
+        response.addCookie(cookie)
+        
+        render([status: 'ok', secure: true, httpOnly: true] as JSON)
+    }
+
+    def setMultipleCookies() {
+        3.times { i ->
+            def cookie = new jakarta.servlet.http.Cookie("cookie${i}", 
"value${i}")
+            cookie.maxAge = 3600
+            cookie.path = '/'
+            response.addCookie(cookie)
+        }
+        render([status: 'ok', cookiesSet: 3] as JSON)
+    }
+
+    def getCookies() {
+        def cookies = request.cookies?.collectEntries { cookie ->
+            [cookie.name, cookie.value]
+        } ?: [:]
+        render([cookies: cookies] as JSON)
+    }
+
+    def getSpecificCookie() {
+        def cookieName = params.name ?: 'testCookie'
+        def cookie = request.cookies?.find { it.name == cookieName }
+        render([
+            found: cookie != null,
+            name: cookie?.name,
+            value: cookie?.value
+        ] as JSON)
+    }
+
+    def deleteCookie() {
+        def cookieName = params.name ?: 'testCookie'
+        def cookie = new jakarta.servlet.http.Cookie(cookieName, '')
+        cookie.maxAge = 0
+        cookie.path = '/'
+        response.addCookie(cookie)
+        render([status: 'ok', deleted: cookieName] as JSON)
+    }
+
+    // ========== Session Tests ==========
+
+    def setSessionAttribute() {
+        def key = params.key ?: 'testKey'
+        def value = params.value ?: 'testValue'
+        session[key] = value
+        render([status: 'ok', sessionId: session.id, key: key, value: value] 
as JSON)
+    }
+
+    def getSessionAttribute() {
+        def key = params.key ?: 'testKey'
+        def value = session[key]
+        render([sessionId: session.id, key: key, value: value, found: value != 
null] as JSON)
+    }
+
+    def getAllSessionAttributes() {
+        def attributes = [:]
+        session.attributeNames.each { name ->
+            // Skip internal attributes
+            if (!name.startsWith('org.') && !name.startsWith('SPRING_')) {
+                attributes[name] = session.getAttribute(name)?.toString()
+            }
+        }
+        render([sessionId: session.id, attributes: attributes] as JSON)
+    }
+
+    def removeSessionAttribute() {
+        def key = params.key ?: 'testKey'
+        def previousValue = session[key]
+        session.removeAttribute(key)
+        render([status: 'ok', key: key, previousValue: previousValue, removed: 
true] as JSON)
+    }
+
+    def invalidateSession() {
+        def oldSessionId = session.id
+        session.invalidate()
+        render([status: 'ok', invalidated: true, oldSessionId: oldSessionId] 
as JSON)
+    }
+
+    def sessionCounter() {
+        def count = session.counter ?: 0
+        count++
+        session.counter = count
+        render([sessionId: session.id, count: count] as JSON)
+    }
+
+    // ========== Request Attribute Tests ==========
+
+    def setRequestAttribute() {
+        def key = params.key ?: 'requestAttr'
+        def value = params.value ?: 'requestValue'
+        request.setAttribute(key, value)
+        // Read it back to verify
+        def retrieved = request.getAttribute(key)
+        render([key: key, setValue: value, retrievedValue: retrieved] as JSON)
+    }
+
+    def getRequestInfo() {
+        render([
+            method: request.method,
+            uri: request.requestURI,
+            url: request.requestURL.toString(),
+            queryString: request.queryString,
+            contextPath: request.contextPath,
+            servletPath: request.servletPath,
+            scheme: request.scheme,
+            serverName: request.serverName,
+            serverPort: request.serverPort,
+            remoteAddr: request.remoteAddr,
+            localAddr: request.localAddr,
+            protocol: request.protocol
+        ] as JSON)
+    }
+
+    def getRequestParameters() {
+        def params = request.parameterMap.collectEntries { k, v ->
+            [k, v.length == 1 ? v[0] : v.toList()]
+        }
+        render([parameters: params] as JSON)
+    }
+
+    // ========== Content Type and Encoding Tests ==========
+
+    def setContentType() {
+        def contentType = params.contentType ?: 'application/json'
+        response.contentType = contentType
+        render([contentType: contentType] as JSON)
+    }
+
+    def setCharacterEncoding() {
+        def encoding = params.encoding ?: 'UTF-8'
+        response.characterEncoding = encoding
+        render([encoding: encoding, message: 'Encoding set to ' + encoding] as 
JSON)
+    }
+
+    def unicodeResponse() {
+        response.characterEncoding = 'UTF-8'
+        render([
+            english: 'Hello World',
+            chinese: '你好世界',
+            japanese: 'こんにちは世界',
+            korean: '안녕하세요 세계',
+            arabic: 'مرحبا بالعالم',
+            emoji: '👋🌍🎉'
+        ] as JSON)
+    }
+}
diff --git 
a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/cors/CorsAdvancedSpec.groovy
 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/cors/CorsAdvancedSpec.groovy
new file mode 100644
index 0000000000..bf86e37560
--- /dev/null
+++ 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/cors/CorsAdvancedSpec.groovy
@@ -0,0 +1,301 @@
+/*
+ *  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
+ *
+ *    https://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 functionaltests.cors
+
+import functionaltests.Application
+import grails.testing.mixin.integration.Integration
+import grails.gorm.transactions.Rollback
+import groovy.json.JsonSlurper
+import io.micronaut.http.HttpRequest
+import io.micronaut.http.HttpResponse
+import io.micronaut.http.HttpStatus
+import io.micronaut.http.MediaType
+import io.micronaut.http.client.HttpClient
+import spock.lang.Specification
+import spock.lang.Shared
+
+/**
+ * Integration tests for CORS (Cross-Origin Resource Sharing) functionality.
+ * Tests preflight requests, CORS headers, and cross-origin scenarios.
+ * 
+ * Note: CORS is enabled for /api/** in application.yml
+ */
+@Integration(applicationClass = Application)
+@Rollback
+class CorsAdvancedSpec extends Specification {
+
+    @Shared
+    HttpClient client
+
+    def setup() {
+        client = HttpClient.create(new URL("http://localhost:${serverPort}";))
+    }
+
+    def cleanup() {
+        client?.close()
+    }
+
+    // ========== Basic CORS Header Tests ==========
+
+    def "GET request to CORS-enabled endpoint includes CORS headers"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/api/cors')
+                .header('Origin', 'http://example.com'),
+            String
+        )
+
+        then:
+        response.status == HttpStatus.OK
+        // CORS headers should be present
+        response.header('Access-Control-Allow-Origin') != null
+    }
+
+    def "OPTIONS preflight request returns appropriate headers"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.OPTIONS('/api/cors/data')
+                .header('Origin', 'http://example.com')
+                .header('Access-Control-Request-Method', 'GET'),
+            String
+        )
+
+        then:
+        response.status == HttpStatus.OK
+        response.header('Access-Control-Allow-Origin') != null
+        response.header('Access-Control-Allow-Methods') != null
+    }
+
+    def "preflight request for POST method succeeds"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.OPTIONS('/api/cors/items')
+                .header('Origin', 'http://example.com')
+                .header('Access-Control-Request-Method', 'POST')
+                .header('Access-Control-Request-Headers', 'Content-Type'),
+            String
+        )
+
+        then:
+        response.status == HttpStatus.OK
+        response.header('Access-Control-Allow-Methods')?.contains('POST') ||
+            response.header('Access-Control-Allow-Origin') != null
+    }
+
+    def "preflight request for PUT method succeeds"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.OPTIONS('/api/cors/items/1')
+                .header('Origin', 'http://example.com')
+                .header('Access-Control-Request-Method', 'PUT')
+                .header('Access-Control-Request-Headers', 'Content-Type'),
+            String
+        )
+
+        then:
+        response.status == HttpStatus.OK
+    }
+
+    def "preflight request for DELETE method succeeds"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.OPTIONS('/api/cors/items/1')
+                .header('Origin', 'http://example.com')
+                .header('Access-Control-Request-Method', 'DELETE'),
+            String
+        )
+
+        then:
+        response.status == HttpStatus.OK
+    }
+
+    // ========== Actual Request Tests ==========
+
+    def "GET request to CORS endpoint returns data"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/api/cors/data')
+                .header('Origin', 'http://example.com'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.data.size() == 3
+        json.total == 3
+    }
+
+    def "POST request with CORS headers succeeds"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/api/cors/items', '{"name":"New Item"}')
+                .header('Origin', 'http://example.com')
+                .contentType(MediaType.APPLICATION_JSON),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.created == true
+        json.method == 'POST'
+    }
+
+    def "PUT request with CORS headers succeeds"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.PUT('/api/cors/items/42', '{"name":"Updated Item"}')
+                .header('Origin', 'http://example.com')
+                .contentType(MediaType.APPLICATION_JSON),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.updated == true
+        json.id == '42'
+        json.method == 'PUT'
+    }
+
+    def "DELETE request with CORS headers succeeds"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.DELETE('/api/cors/items/99')
+                .header('Origin', 'http://example.com'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.deleted == true
+        json.id == '99'
+        json.method == 'DELETE'
+    }
+
+    // ========== Custom Headers Tests ==========
+
+    def "response with custom headers includes CORS headers"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/api/cors/custom-headers')
+                .header('Origin', 'http://example.com'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.customHeadersSet == true
+        response.header('X-Custom-Response') == 'custom-value'
+    }
+
+    def "echo origin endpoint returns received origin"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/api/cors/echo-origin')
+                .header('Origin', 'http://my-app.example.com'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.receivedOrigin == 'http://my-app.example.com'
+    }
+
+    // ========== Credentials Tests ==========
+
+    def "authenticated endpoint receives authorization header"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/api/cors/authenticated')
+                .header('Origin', 'http://example.com')
+                .header('Authorization', 'Bearer test-token-123'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.authenticated == true
+        json.authType == 'Bearer'
+    }
+
+    def "authenticated endpoint without credentials returns unauthenticated"() 
{
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/api/cors/authenticated')
+                .header('Origin', 'http://example.com'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.authenticated == false
+        json.message == 'No credentials'
+    }
+
+    // ========== Various Origin Tests ==========
+
+    def "request from localhost origin succeeds"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/api/cors')
+                .header('Origin', 'http://localhost:3000'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.message == 'CORS test endpoint'
+    }
+
+    def "request from HTTPS origin succeeds"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/api/cors')
+                .header('Origin', 'https://secure.example.com'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.message == 'CORS test endpoint'
+    }
+
+    def "request with port in origin succeeds"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/api/cors')
+                .header('Origin', 'http://example.com:8080'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.message == 'CORS test endpoint'
+    }
+}
diff --git 
a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/errorhandling/ErrorHandlingSpec.groovy
 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/errorhandling/ErrorHandlingSpec.groovy
new file mode 100644
index 0000000000..9c28026cbc
--- /dev/null
+++ 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/errorhandling/ErrorHandlingSpec.groovy
@@ -0,0 +1,363 @@
+/*
+ *  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
+ *
+ *    https://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 functionaltests.errorhandling
+
+import functionaltests.Application
+import grails.testing.mixin.integration.Integration
+import grails.gorm.transactions.Rollback
+import groovy.json.JsonSlurper
+import io.micronaut.http.HttpRequest
+import io.micronaut.http.HttpResponse
+import io.micronaut.http.HttpStatus
+import io.micronaut.http.client.HttpClient
+import io.micronaut.http.client.exceptions.HttpClientResponseException
+import spock.lang.Specification
+import spock.lang.Shared
+import spock.lang.Unroll
+
+/**
+ * Integration tests for error handling patterns in Grails controllers.
+ * Tests various HTTP status codes, JSON error responses, exception handling,
+ * and error response headers.
+ */
+@Integration(applicationClass = Application)
+@Rollback
+class ErrorHandlingSpec extends Specification {
+
+    @Shared
+    HttpClient client
+
+    def setup() {
+        client = HttpClient.create(new URL("http://localhost:${serverPort}";))
+    }
+
+    def cleanup() {
+        client?.close()
+    }
+
+    // ========== HTTP Status Code Tests ==========
+
+    def "render 404 Not Found status"() {
+        when:
+        client.toBlocking().exchange(
+            HttpRequest.GET('/errorHandlingTest/renderNotFound'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.NOT_FOUND
+    }
+
+    def "render 400 Bad Request status"() {
+        when:
+        client.toBlocking().exchange(
+            HttpRequest.GET('/errorHandlingTest/renderBadRequest'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.BAD_REQUEST
+    }
+
+    def "render 401 Unauthorized status"() {
+        when:
+        client.toBlocking().exchange(
+            HttpRequest.GET('/errorHandlingTest/renderUnauthorized'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.UNAUTHORIZED
+    }
+
+    def "render 403 Forbidden status"() {
+        when:
+        client.toBlocking().exchange(
+            HttpRequest.GET('/errorHandlingTest/renderForbidden'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.FORBIDDEN
+    }
+
+    def "render 405 Method Not Allowed status"() {
+        when:
+        client.toBlocking().exchange(
+            HttpRequest.GET('/errorHandlingTest/renderMethodNotAllowed'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.METHOD_NOT_ALLOWED
+    }
+
+    def "render 409 Conflict status"() {
+        when:
+        client.toBlocking().exchange(
+            HttpRequest.GET('/errorHandlingTest/renderConflict'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.CONFLICT
+    }
+
+    def "render 410 Gone status"() {
+        when:
+        client.toBlocking().exchange(
+            HttpRequest.GET('/errorHandlingTest/renderGone'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.GONE
+    }
+
+    def "render 422 Unprocessable Entity status"() {
+        when:
+        client.toBlocking().exchange(
+            HttpRequest.GET('/errorHandlingTest/renderUnprocessableEntity'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.UNPROCESSABLE_ENTITY
+    }
+
+    def "render 429 Too Many Requests status"() {
+        when:
+        client.toBlocking().exchange(
+            HttpRequest.GET('/errorHandlingTest/renderTooManyRequests'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.TOO_MANY_REQUESTS
+    }
+
+    def "render 500 Internal Server Error status"() {
+        when:
+        client.toBlocking().exchange(
+            HttpRequest.GET('/errorHandlingTest/renderInternalServerError'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.INTERNAL_SERVER_ERROR
+    }
+
+    def "render 503 Service Unavailable status"() {
+        when:
+        client.toBlocking().exchange(
+            HttpRequest.GET('/errorHandlingTest/renderServiceUnavailable'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.SERVICE_UNAVAILABLE
+    }
+
+    // ========== JSON Error Response Tests ==========
+
+    def "JSON 404 error response contains proper structure"() {
+        when:
+        client.toBlocking().exchange(
+            
HttpRequest.GET('/errorHandlingTest/jsonNotFound').accept('application/json'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.NOT_FOUND
+    }
+
+    def "JSON 400 error response with validation details"() {
+        when:
+        client.toBlocking().exchange(
+            
HttpRequest.GET('/errorHandlingTest/jsonBadRequest').accept('application/json'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.BAD_REQUEST
+    }
+
+    def "JSON 422 validation error with multiple field errors"() {
+        when:
+        client.toBlocking().exchange(
+            
HttpRequest.GET('/errorHandlingTest/jsonValidationError').accept('application/json'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.UNPROCESSABLE_ENTITY
+    }
+
+    def "JSON 500 error response includes request ID"() {
+        when:
+        client.toBlocking().exchange(
+            
HttpRequest.GET('/errorHandlingTest/jsonServerError').accept('application/json'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.INTERNAL_SERVER_ERROR
+    }
+
+    // ========== Conditional Error Handling Tests ==========
+
+    def "conditional error returns 404 when condition is notfound"() {
+        when:
+        client.toBlocking().exchange(
+            
HttpRequest.GET('/errorHandlingTest/conditionalError?condition=notfound').accept('application/json'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.NOT_FOUND
+    }
+
+    def "conditional error returns 400 when condition is badrequest"() {
+        when:
+        client.toBlocking().exchange(
+            
HttpRequest.GET('/errorHandlingTest/conditionalError?condition=badrequest').accept('application/json'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.BAD_REQUEST
+    }
+
+    def "conditional error returns 403 when condition is forbidden"() {
+        when:
+        client.toBlocking().exchange(
+            
HttpRequest.GET('/errorHandlingTest/conditionalError?condition=forbidden').accept('application/json'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.FORBIDDEN
+    }
+
+    def "conditional error returns success for unknown condition"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            
HttpRequest.GET('/errorHandlingTest/conditionalError?condition=normal').accept('application/json'),
+            String
+        )
+
+        then:
+        response.status == HttpStatus.OK
+
+        and:
+        def json = new JsonSlurper().parseText(response.body())
+        json.status == 'ok'
+        json.condition == 'normal'
+    }
+
+    // ========== Error Response Headers Tests ==========
+
+    def "rate limit error includes appropriate headers"() {
+        when:
+        client.toBlocking().exchange(
+            
HttpRequest.GET('/errorHandlingTest/errorWithHeaders').accept('application/json'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.TOO_MANY_REQUESTS
+
+        and:
+        def response = e.response
+        response.header('Retry-After') == '60'
+        response.header('X-RateLimit-Limit') == '100'
+        response.header('X-RateLimit-Remaining') == '0'
+        response.header('X-RateLimit-Reset') != null
+    }
+
+    def "not found error includes suggestion header"() {
+        when:
+        client.toBlocking().exchange(
+            
HttpRequest.GET('/errorHandlingTest/notFoundWithHints').accept('application/json'),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.NOT_FOUND
+
+        and: "suggestion header is present"
+        e.response.header('X-Suggested-Resource') == '/api/items'
+    }
+
+    // ========== Success Comparison Tests ==========
+
+    def "success endpoint returns 200 OK"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            
HttpRequest.GET('/errorHandlingTest/success').accept('application/json'),
+            String
+        )
+
+        then:
+        response.status == HttpStatus.OK
+
+        and:
+        def json = new JsonSlurper().parseText(response.body())
+        json.status == 'ok'
+        json.message == 'Operation successful'
+    }
+
+    def "success with data returns structured response"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            
HttpRequest.GET('/errorHandlingTest/successWithData').accept('application/json'),
+            String
+        )
+
+        then:
+        response.status == HttpStatus.OK
+
+        and:
+        def json = new JsonSlurper().parseText(response.body())
+        json.status == 'ok'
+        json.data.id == 1
+        json.data.name == 'Test Item'
+        json.data.createdAt != null
+    }
+}
diff --git 
a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy
 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy
new file mode 100644
index 0000000000..a81b33bbd5
--- /dev/null
+++ 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy
@@ -0,0 +1,395 @@
+/*
+ *  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
+ *
+ *    https://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 functionaltests.fileupload
+
+import functionaltests.Application
+import grails.testing.mixin.integration.Integration
+import grails.gorm.transactions.Rollback
+import groovy.json.JsonSlurper
+import io.micronaut.http.HttpRequest
+import io.micronaut.http.HttpResponse
+import io.micronaut.http.HttpStatus
+import io.micronaut.http.MediaType
+import io.micronaut.http.client.HttpClient
+import io.micronaut.http.client.exceptions.HttpClientResponseException
+import io.micronaut.http.client.multipart.MultipartBody
+import spock.lang.Specification
+import spock.lang.Shared
+
+/**
+ * Integration tests for file upload functionality in Grails.
+ * Tests various file upload patterns including single file, multiple files,
+ * file validation, and metadata extraction.
+ */
+@Integration(applicationClass = Application)
+@Rollback
+class FileUploadSpec extends Specification {
+
+    @Shared
+    HttpClient client
+
+    def setup() {
+        client = HttpClient.create(new URL("http://localhost:${serverPort}";))
+    }
+
+    def cleanup() {
+        client?.close()
+    }
+
+    // ========== Single File Upload Tests ==========
+
+    def "upload single text file returns file info"() {
+        given:
+        def content = 'Hello, this is a test file content!'
+        def body = MultipartBody.builder()
+            .addPart('file', 'test.txt', MediaType.TEXT_PLAIN_TYPE, 
content.bytes)
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/uploadSingle', body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.success == true
+        json.filename == 'test.txt'
+        json.size == content.bytes.length
+    }
+
+    def "upload single file without file returns error"() {
+        given:
+        def body = MultipartBody.builder()
+            .addPart('other', 'value')
+            .build()
+
+        when:
+        client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/uploadSingle', body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+
+        then:
+        HttpClientResponseException e = thrown()
+        e.status == HttpStatus.BAD_REQUEST
+    }
+
+    def "upload file with metadata includes description and category"() {
+        given:
+        def content = 'File with metadata'
+        def body = MultipartBody.builder()
+            .addPart('file', 'data.txt', MediaType.TEXT_PLAIN_TYPE, 
content.bytes)
+            .addPart('description', 'My test file')
+            .addPart('category', 'documents')
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/uploadWithMetadata', body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.success == true
+        json.description == 'My test file'
+        json.category == 'documents'
+    }
+
+    // ========== Multiple File Upload Tests ==========
+
+    def "upload multiple files returns all file info"() {
+        given:
+        def body = MultipartBody.builder()
+            .addPart('files', 'file1.txt', MediaType.TEXT_PLAIN_TYPE, 'Content 
1'.bytes)
+            .addPart('files', 'file2.txt', MediaType.TEXT_PLAIN_TYPE, 'Content 
2'.bytes)
+            .addPart('files', 'file3.txt', MediaType.TEXT_PLAIN_TYPE, 'Content 
3'.bytes)
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/uploadMultiple', body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.success == true
+        json.count == 3
+        json.files.size() == 3
+        json.files*.filename.containsAll(['file1.txt', 'file2.txt', 
'file3.txt'])
+    }
+
+    // ========== File Content Processing Tests ==========
+
+    def "upload text file returns line and word count"() {
+        given:
+        def content = '''Line 1
+Line 2
+Line 3
+This is a longer line with more words'''
+        def body = MultipartBody.builder()
+            .addPart('file', 'multiline.txt', MediaType.TEXT_PLAIN_TYPE, 
content.bytes)
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/uploadTextFile', body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.success == true
+        json.lineCount == 4
+        json.wordCount > 0
+        json.preview.startsWith('Line 1')
+    }
+
+    def "upload and echo returns original content"() {
+        given:
+        def content = 'Echo this content back to me!'
+        def body = MultipartBody.builder()
+            .addPart('file', 'echo.txt', MediaType.TEXT_PLAIN_TYPE, 
content.bytes)
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/uploadAndEcho', body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.success == true
+        json.content == content
+    }
+
+    // ========== File Validation Tests ==========
+
+    def "upload file with allowed type passes validation"() {
+        given:
+        def body = MultipartBody.builder()
+            .addPart('file', 'valid.txt', MediaType.TEXT_PLAIN_TYPE, 'Valid 
content'.bytes)
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/uploadWithValidation', body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.success == true
+        json.validated == true
+    }
+
+    def "upload file with valid extension passes validation"() {
+        given:
+        def body = MultipartBody.builder()
+            .addPart('file', 'data.json', MediaType.APPLICATION_JSON_TYPE, 
'{"key":"value"}'.bytes)
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/uploadWithExtensionValidation', 
body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.success == true
+        json.validated == true
+        json.extension == 'json'
+    }
+
+    def "upload file with csv extension passes validation"() {
+        given:
+        def csvContent = 'name,age,city\nJohn,30,NYC\nJane,25,LA'
+        def body = MultipartBody.builder()
+            .addPart('file', 'data.csv', MediaType.of('text/csv'), 
csvContent.bytes)
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/uploadWithExtensionValidation', 
body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.success == true
+        json.extension == 'csv'
+    }
+
+    // ========== File Info Extraction Tests ==========
+
+    def "get file info extracts all metadata"() {
+        given:
+        def body = MultipartBody.builder()
+            .addPart('file', 'document.txt', MediaType.TEXT_PLAIN_TYPE, 'Some 
content here'.bytes)
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/getFileInfo', body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.originalFilename == 'document.txt'
+        json.basename == 'document'
+        json.extension == 'txt'
+        json.size == 'Some content here'.bytes.length
+        json.isEmpty == false
+    }
+
+    def "get file info handles filename without extension"() {
+        given:
+        def body = MultipartBody.builder()
+            .addPart('file', 'README', MediaType.TEXT_PLAIN_TYPE, 'Readme 
content'.bytes)
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/getFileInfo', body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.originalFilename == 'README'
+        json.basename == 'README'
+        json.extension == ''
+    }
+
+    // ========== Params Access Tests ==========
+
+    def "upload via params accesses file correctly"() {
+        given:
+        def body = MultipartBody.builder()
+            .addPart('file', 'params-test.txt', MediaType.TEXT_PLAIN_TYPE, 
'Accessed via params'.bytes)
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/uploadViaParams', body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.success == true
+        json.accessedViaParams == true
+        json.filename == 'params-test.txt'
+    }
+
+    // ========== Large File Tests ==========
+
+    def "upload larger text file succeeds"() {
+        given:
+        def content = ('X' * 1000) // 1KB of X characters
+        def body = MultipartBody.builder()
+            .addPart('file', 'large.txt', MediaType.TEXT_PLAIN_TYPE, 
content.bytes)
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/uploadSingle', body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.success == true
+        json.size == 1000
+    }
+
+    def "upload json file with content"() {
+        given:
+        def jsonContent = 
'{"users":[{"name":"Alice","age":30},{"name":"Bob","age":25}]}'
+        def body = MultipartBody.builder()
+            .addPart('file', 'users.json', MediaType.APPLICATION_JSON_TYPE, 
jsonContent.bytes)
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/uploadAndEcho', body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.success == true
+        json.filename == 'users.json'
+        json.content == jsonContent
+    }
+
+    def "upload xml file with content"() {
+        given:
+        def xmlContent = '<?xml version="1.0"?><root><item 
id="1">Test</item></root>'
+        def body = MultipartBody.builder()
+            .addPart('file', 'data.xml', MediaType.APPLICATION_XML_TYPE, 
xmlContent.bytes)
+            .build()
+
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/fileUploadTest/uploadAndEcho', body)
+                .contentType(MediaType.MULTIPART_FORM_DATA_TYPE),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.success == true
+        json.filename == 'data.xml'
+        json.content == xmlContent
+    }
+}
diff --git 
a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/requestresponse/RequestResponseSpec.groovy
 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/requestresponse/RequestResponseSpec.groovy
new file mode 100644
index 0000000000..40f907bbad
--- /dev/null
+++ 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/requestresponse/RequestResponseSpec.groovy
@@ -0,0 +1,388 @@
+/*
+ *  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
+ *
+ *    https://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 functionaltests.requestresponse
+
+import functionaltests.Application
+import grails.testing.mixin.integration.Integration
+import grails.gorm.transactions.Rollback
+import groovy.json.JsonSlurper
+import io.micronaut.http.HttpRequest
+import io.micronaut.http.HttpResponse
+import io.micronaut.http.HttpStatus
+import io.micronaut.http.MutableHttpRequest
+import io.micronaut.http.client.HttpClient
+import io.micronaut.http.cookie.Cookie
+import spock.lang.Specification
+import spock.lang.Shared
+
+/**
+ * Integration tests for request/response handling patterns including
+ * headers, cookies, session management, and request attributes.
+ */
+@Integration(applicationClass = Application)
+@Rollback
+class RequestResponseSpec extends Specification {
+
+    @Shared
+    HttpClient client
+
+    def setup() {
+        client = HttpClient.create(new URL("http://localhost:${serverPort}";))
+    }
+
+    def cleanup() {
+        client?.close()
+    }
+
+    /**
+     * Helper method to find header value case-insensitively.
+     * HTTP headers are case-insensitive per spec.
+     */
+    private String findHeader(Map headers, String name) {
+        def entry = headers.find { k, v -> k.equalsIgnoreCase(name) }
+        return entry?.value
+    }
+
+    // ========== Request Header Tests ==========
+
+    def "echo request headers returns all headers"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/requestResponseTest/echoHeaders')
+                .header('X-Custom-Header', 'TestValue')
+                .header('X-Another-Header', 'AnotherValue'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        findHeader(json.headers, 'X-Custom-Header') == 'TestValue'
+        findHeader(json.headers, 'X-Another-Header') == 'AnotherValue'
+    }
+
+    def "get specific header returns correct value"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            
HttpRequest.GET('/requestResponseTest/getSpecificHeader?headerName=X-Test-Header')
+                .header('X-Test-Header', 'MyTestValue'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        // Header name might be normalized/lowercased
+        json.headerValue == 'MyTestValue'
+    }
+
+    def "check accept header detects JSON accept type"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/requestResponseTest/checkAcceptHeader')
+                .accept('application/json'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.acceptsJson == true
+    }
+
+    // ========== Response Header Tests ==========
+
+    def "set custom headers returns headers in response"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/requestResponseTest/setCustomHeaders'),
+            String
+        )
+
+        then:
+        response.status == HttpStatus.OK
+        response.header('X-Custom-Header') == 'CustomValue'
+        response.header('X-Request-Id') != null
+        response.header('X-Timestamp') != null
+    }
+
+    def "set cache headers configures caching properly"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/requestResponseTest/setCacheHeaders'),
+            String
+        )
+
+        then:
+        response.status == HttpStatus.OK
+        response.header('Cache-Control') == 'max-age=3600, public'
+        response.header('ETag') == '"abc123"'
+        response.header('Last-Modified') != null
+    }
+
+    def "set no-cache headers prevents caching"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/requestResponseTest/setNoCacheHeaders'),
+            String
+        )
+
+        then:
+        response.status == HttpStatus.OK
+        response.header('Cache-Control') == 'no-cache, no-store, 
must-revalidate'
+        response.header('Pragma') == 'no-cache'
+        response.header('Expires') == '0'
+    }
+
+    def "set content disposition for file download"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/requestResponseTest/setContentDisposition'),
+            String
+        )
+
+        then:
+        response.status == HttpStatus.OK
+        response.header('Content-Disposition') == 'attachment; 
filename="report.pdf"'
+    }
+
+    def "set multiple custom headers returns all headers"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/requestResponseTest/setMultipleCustomHeaders'),
+            String
+        )
+
+        then:
+        response.status == HttpStatus.OK
+        response.header('X-Custom-0') == 'Value-0'
+        response.header('X-Custom-1') == 'Value-1'
+        response.header('X-Custom-4') == 'Value-4'
+    }
+
+    // ========== Cookie Tests ==========
+
+    def "set cookie returns Set-Cookie header"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            
HttpRequest.GET('/requestResponseTest/setCookie?name=myCookie&value=myValue'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.cookieSet == true
+        json.name == 'myCookie'
+        json.value == 'myValue'
+        response.header('Set-Cookie')?.contains('myCookie=myValue')
+    }
+
+    def "set multiple cookies returns multiple Set-Cookie headers"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/requestResponseTest/setMultipleCookies'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.cookiesSet == 3
+        response.headers.getAll('Set-Cookie').size() >= 3
+    }
+
+    def "get cookies reads cookies from request"() {
+        when:
+        MutableHttpRequest<Object> request = 
HttpRequest.GET('/requestResponseTest/getCookies')
+            .cookie(Cookie.of('testCookie1', 'value1'))
+            .cookie(Cookie.of('testCookie2', 'value2'))
+        HttpResponse<String> response = client.toBlocking().exchange(request, 
String)
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.cookies['testCookie1'] == 'value1'
+        json.cookies['testCookie2'] == 'value2'
+    }
+
+    def "get specific cookie returns correct cookie value"() {
+        when:
+        MutableHttpRequest<Object> request = 
HttpRequest.GET('/requestResponseTest/getSpecificCookie?name=myCookie')
+            .cookie(Cookie.of('myCookie', 'cookieValue'))
+        HttpResponse<String> response = client.toBlocking().exchange(request, 
String)
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.found == true
+        json.name == 'myCookie'
+        json.value == 'cookieValue'
+    }
+
+    def "delete cookie sets max-age to 0"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            
HttpRequest.GET('/requestResponseTest/deleteCookie?name=deletedCookie'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.deleted == 'deletedCookie'
+        response.header('Set-Cookie')?.contains('Max-Age=0') || 
response.header('Set-Cookie')?.contains('Expires=')
+    }
+
+    // ========== Session Tests ==========
+
+    def "set session attribute stores value in session"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            
HttpRequest.GET('/requestResponseTest/setSessionAttribute?key=testKey&value=testValue'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.key == 'testKey'
+        json.value == 'testValue'
+        json.sessionId != null
+    }
+
+    def "get session attribute retrieves stored value"() {
+        given:
+        // First set a session attribute
+        HttpResponse<String> setResponse = client.toBlocking().exchange(
+            
HttpRequest.GET('/requestResponseTest/setSessionAttribute?key=retrieveKey&value=retrieveValue'),
+            String
+        )
+        def sessionCookie = setResponse.header('Set-Cookie')
+
+        when:
+        // Then retrieve it with the same session
+        MutableHttpRequest<Object> getRequest = 
HttpRequest.GET('/requestResponseTest/getSessionAttribute?key=retrieveKey')
+        if (sessionCookie) {
+            def cookieValue = sessionCookie.split(';')[0]
+            getRequest = getRequest.header('Cookie', cookieValue)
+        }
+        HttpResponse<String> response = 
client.toBlocking().exchange(getRequest, String)
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.key == 'retrieveKey'
+        json.value == 'retrieveValue'
+        json.found == true
+    }
+
+    def "session counter increments on each request"() {
+        given:
+        // First request
+        HttpResponse<String> response1 = client.toBlocking().exchange(
+            HttpRequest.GET('/requestResponseTest/sessionCounter'),
+            String
+        )
+        def json1 = new JsonSlurper().parseText(response1.body())
+        def sessionCookie = response1.header('Set-Cookie')
+
+        when:
+        // Second request with same session
+        MutableHttpRequest<Object> request2 = 
HttpRequest.GET('/requestResponseTest/sessionCounter')
+        if (sessionCookie) {
+            def cookieValue = sessionCookie.split(';')[0]
+            request2 = request2.header('Cookie', cookieValue)
+        }
+        HttpResponse<String> response2 = 
client.toBlocking().exchange(request2, String)
+        def json2 = new JsonSlurper().parseText(response2.body())
+
+        then:
+        response1.status == HttpStatus.OK
+        response2.status == HttpStatus.OK
+        json1.count == 1
+        json2.count == 2
+    }
+
+    // ========== Request Info Tests ==========
+
+    def "get request info returns server details"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/requestResponseTest/getRequestInfo'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.method == 'GET'
+        json.uri == '/requestResponseTest/getRequestInfo'
+        json.scheme == 'http'
+        json.serverPort == serverPort
+    }
+
+    def "get request parameters returns query parameters"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            
HttpRequest.GET('/requestResponseTest/getRequestParameters?param1=value1&param2=value2'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.parameters['param1'] == 'value1'
+        json.parameters['param2'] == 'value2'
+    }
+
+    def "set request attribute stores and retrieves value"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            
HttpRequest.GET('/requestResponseTest/setRequestAttribute?key=myAttr&value=myVal'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.key == 'myAttr'
+        json.setValue == 'myVal'
+        json.retrievedValue == 'myVal'
+    }
+
+    // ========== Content Type and Encoding Tests ==========
+
+    def "unicode response returns characters in multiple languages"() {
+        when:
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/requestResponseTest/unicodeResponse'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then:
+        response.status == HttpStatus.OK
+        json.english == 'Hello World'
+        json.chinese == '你好世界'
+        json.japanese == 'こんにちは世界'
+        json.korean == '안녕하세요 세계'
+        json.emoji == '👋🌍🎉'
+    }
+}

Reply via email to