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 c602f2819c07454dba5dee207a01403e7bfc07a6
Author: James Fredley <[email protected]>
AuthorDate: Sun Jan 25 22:05:10 2026 -0500

    Add async/promises and interceptor integration tests
    
    - Add AsyncPromisesSpec with 20 tests for Grails async features
    - Tests Promise.task, Promise.onComplete/onError callbacks
    - Tests PromiseList, PromiseMap, and timeout handling
    - Add InterceptorSpec with 20 tests for controller interceptors
    - Tests before/after/afterView interceptor phases
    - Tests matching patterns, model modification, request blocking
---
 .../async/AsyncTestController.groovy               | 285 +++++++++++++++
 .../AttributeSettingInterceptor.groovy             |  46 +++
 .../interceptors/BlockingInterceptor.groovy        |  47 +++
 .../ConditionalMatchInterceptor.groovy             |  52 +++
 .../interceptors/FirstInterceptor.groovy           |  48 +++
 .../interceptors/InterceptorTestController.groovy  | 172 +++++++++
 .../interceptors/SecondInterceptor.groovy          |  48 +++
 .../interceptors/SessionInterceptor.groovy         |  44 +++
 .../interceptors/ThirdInterceptor.groovy           |  48 +++
 .../interceptors/TimingInterceptor.groovy          |  46 +++
 .../functionaltests/async/AsyncPromiseSpec.groovy  | 390 ++++++++++++++++++++
 .../interceptors/InterceptorOrderingSpec.groovy    | 407 +++++++++++++++++++++
 12 files changed, 1633 insertions(+)

diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/async/AsyncTestController.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/async/AsyncTestController.groovy
new file mode 100644
index 0000000000..763062c3df
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/async/AsyncTestController.groovy
@@ -0,0 +1,285 @@
+/*
+ *  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.async
+
+import functionaltests.services.AsyncProcessingService
+import grails.converters.JSON
+
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+
+import static grails.async.web.WebPromises.*
+
+/**
+ * Controller demonstrating async request handling patterns in Grails.
+ */
+class AsyncTestController {
+
+    static responseFormats = ['json', 'html']
+
+    AsyncProcessingService asyncProcessingService
+
+    /**
+     * Simple async task that completes after a delay.
+     */
+    def simpleTask() {
+        task {
+            sleep 100
+            render([status: 'completed', message: 'Task finished'] as JSON)
+        }
+    }
+
+    /**
+     * Async task returning a computed value.
+     */
+    def computeTask() {
+        def input = params.int('value', 10)
+        task {
+            sleep 50
+            def result = input * input
+            render([input: input, result: result] as JSON)
+        }
+    }
+
+    /**
+     * Multiple async tasks - simulates parallel work, returns combined 
results.
+     */
+    def parallelTasks() {
+        task {
+            // Simulate parallel work by doing multiple operations
+            def result1 = 'Task 1 result'
+            def result2 = 'Task 2 result'
+            def result3 = 'Task 3 result'
+            sleep 50
+            def results = [result1, result2, result3]
+            render([
+                status: 'completed',
+                results: results
+            ] as JSON)
+        }
+    }
+
+    /**
+     * Chained async tasks.
+     */
+    def chainedTasks() {
+        def input = params.input ?: 'test'
+        
+        task {
+            sleep 50
+            return input.toUpperCase()
+        }.then { result ->
+            sleep 50
+            return result.reverse()
+        }.then { result ->
+            render([
+                original: input,
+                final: result
+            ] as JSON)
+        }
+    }
+
+    /**
+     * Async task with error handling.
+     */
+    def taskWithError() {
+        def shouldFail = params.boolean('fail', false)
+        
+        task {
+            sleep 50
+            if (shouldFail) {
+                throw new RuntimeException('Intentional async error')
+            }
+            return 'Success'
+        }.onComplete { result ->
+            render([
+                status: 'success',
+                result: result
+            ] as JSON)
+        }
+    }
+
+    /**
+     * Timeout handling for async task.
+     */
+    def taskWithTimeout() {
+        def delay = params.int('delay', 100)
+        def timeout = params.int('timeout', 500)
+
+        def startTime = System.currentTimeMillis()
+        task {
+            sleep delay
+            return 'Completed in time'
+        }.onComplete { result ->
+            def elapsed = System.currentTimeMillis() - startTime
+            render([
+                status: 'completed',
+                result: result,
+                elapsedMs: elapsed
+            ] as JSON)
+        }
+    }
+
+    /**
+     * Use the async service directly.
+     */
+    def useAsyncService() {
+        def input = params.input ?: 'hello'
+        
+        def future = asyncProcessingService.processAsync(input)
+        
+        // Wait for result (up to 5 seconds)
+        def result = future.get(5, TimeUnit.SECONDS)
+        
+        render([
+            input: input,
+            result: result
+        ] as JSON)
+    }
+
+    /**
+     * Async service with calculation.
+     */
+    def asyncCalculation() {
+        def value = params.int('value', 5)
+        
+        def future = asyncProcessingService.calculateAsync(value)
+        def result = future.get(5, TimeUnit.SECONDS)
+        
+        render([
+            input: value,
+            squared: result
+        ] as JSON)
+    }
+
+    /**
+     * Batch processing with async service.
+     */
+    def asyncBatch() {
+        def items = params.list('items') ?: ['one', 'two', 'three']
+        
+        def future = asyncProcessingService.processBatchAsync(items)
+        def results = future.get(5, TimeUnit.SECONDS)
+        
+        render([
+            original: items,
+            processed: results
+        ] as JSON)
+    }
+
+    /**
+     * Long-running operation.
+     */
+    def longRunning() {
+        def taskId = params.taskId ?: UUID.randomUUID().toString()
+        
+        def future = asyncProcessingService.longRunningOperation(taskId)
+        def result = future.get(5, TimeUnit.SECONDS)
+        
+        render(result as JSON)
+    }
+
+    /**
+     * CompletableFuture composition.
+     */
+    def composeFutures() {
+        def value1 = params.int('v1', 3)
+        def value2 = params.int('v2', 4)
+        
+        def future1 = asyncProcessingService.calculateAsync(value1)
+        def future2 = asyncProcessingService.calculateAsync(value2)
+        
+        def combined = future1.thenCombine(future2) { r1, r2 ->
+            [
+                value1Squared: r1,
+                value2Squared: r2,
+                sum: r1 + r2
+            ]
+        }
+        
+        def result = combined.get(5, TimeUnit.SECONDS)
+        render(result as JSON)
+    }
+
+    /**
+     * Async task that processes request data.
+     */
+    def processRequestData() {
+        def data = request.JSON ?: [value: 'default']
+        
+        task {
+            sleep 50
+            def processed = data.collectEntries { k, v ->
+                [(k): v?.toString()?.toUpperCase()]
+            }
+            return processed
+        }.onComplete { result ->
+            render([
+                original: data,
+                processed: result
+            ] as JSON)
+        }
+    }
+
+    /**
+     * Demonstrates async response with multiple stages reporting.
+     */
+    def multiStageProcess() {
+        task {
+            def t1 = System.currentTimeMillis()
+            sleep 30
+            def t2 = System.currentTimeMillis()
+            sleep 30
+            def t3 = System.currentTimeMillis()
+            
+            def stages = [
+                [stage: 1, action: 'initialize', time: t1],
+                [stage: 2, action: 'process', time: t2],
+                [stage: 3, action: 'finalize', time: t3]
+            ]
+            render([
+                status: 'completed',
+                stages: stages,
+                totalStages: stages.size()
+            ] as JSON)
+        }
+    }
+
+    /**
+     * Conditional async execution.
+     */
+    def conditionalAsync() {
+        def useAsync = params.boolean('async', true)
+        def input = params.input ?: 'test'
+        
+        if (useAsync) {
+            task {
+                sleep 50
+                return "Async: ${input.toUpperCase()}"
+            }.onComplete { result ->
+                render([mode: 'async', result: result] as JSON)
+            }
+        } else {
+            // Synchronous execution
+            def result = "Sync: ${input.toUpperCase()}"
+            render([mode: 'sync', result: result] as JSON)
+        }
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/AttributeSettingInterceptor.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/AttributeSettingInterceptor.groovy
new file mode 100644
index 0000000000..58ce13e50b
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/AttributeSettingInterceptor.groovy
@@ -0,0 +1,46 @@
+/*
+ *  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.interceptors
+
+/**
+ * Interceptor that sets request attributes and response headers.
+ */
+class AttributeSettingInterceptor {
+
+    AttributeSettingInterceptor() {
+        match(controller: 'interceptorTest', action: 'checkAttributes')
+    }
+
+    boolean before() {
+        InterceptorTestController.executionOrder << "attribute:before"
+        request.setAttribute('beforeInterceptorRan', true)
+        request.setAttribute('interceptorData', [
+            timestamp: System.currentTimeMillis(),
+            source: 'AttributeSettingInterceptor'
+        ])
+        response.setHeader('X-Interceptor-Header', 'set-by-interceptor')
+        true
+    }
+
+    boolean after() {
+        InterceptorTestController.executionOrder << "attribute:after"
+        true
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/BlockingInterceptor.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/BlockingInterceptor.groovy
new file mode 100644
index 0000000000..5224cb11b7
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/BlockingInterceptor.groovy
@@ -0,0 +1,47 @@
+/*
+ *  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.interceptors
+
+/**
+ * Interceptor that can block requests.
+ * Returns false from before() to prevent controller execution.
+ */
+class BlockingInterceptor {
+
+    int order = 5  // Runs before other interceptors
+
+    BlockingInterceptor() {
+        match(controller: 'interceptorTest', action: 'blocked')
+    }
+
+    boolean before() {
+        InterceptorTestController.executionOrder << "blocking:before"
+        if (params.block == 'true') {
+            render(text: '{"blocked":true,"message":"Request blocked by 
interceptor","reason":"' + (params.reason ?: 'No reason provided') + '"}', 
contentType: 'application/json')
+            return false  // Block the request
+        }
+        true
+    }
+
+    boolean after() {
+        InterceptorTestController.executionOrder << "blocking:after"
+        true
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ConditionalMatchInterceptor.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ConditionalMatchInterceptor.groovy
new file mode 100644
index 0000000000..e72f29c009
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ConditionalMatchInterceptor.groovy
@@ -0,0 +1,52 @@
+/*
+ *  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.interceptors
+
+/**
+ * Interceptor with conditional matching using closure.
+ */
+class ConditionalMatchInterceptor {
+
+    ConditionalMatchInterceptor() {
+        match(controller: 'interceptorTest', action: 'conditionalAction')
+            .except(action: 'index')
+    }
+
+    boolean before() {
+        // Only add to execution order if 'match' param is 'yes'
+        if (params.match == 'yes') {
+            InterceptorTestController.executionOrder << 
"conditional:before:matched"
+            request.setAttribute('conditionalMatched', true)
+        } else {
+            InterceptorTestController.executionOrder << 
"conditional:before:notmatched"
+            request.setAttribute('conditionalMatched', false)
+        }
+        true
+    }
+
+    boolean after() {
+        if (params.match == 'yes') {
+            InterceptorTestController.executionOrder << 
"conditional:after:matched"
+        } else {
+            InterceptorTestController.executionOrder << 
"conditional:after:notmatched"
+        }
+        true
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/FirstInterceptor.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/FirstInterceptor.groovy
new file mode 100644
index 0000000000..ddba8a9cef
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/FirstInterceptor.groovy
@@ -0,0 +1,48 @@
+/*
+ *  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.interceptors
+
+/**
+ * First interceptor in the chain (order = 10).
+ * Tests basic before/after functionality.
+ */
+class FirstInterceptor {
+
+    int order = 10
+
+    FirstInterceptor() {
+        match(controller: 'interceptorTest', action: 
~/(index|testOrder|dataAction)/)
+    }
+
+    boolean before() {
+        InterceptorTestController.executionOrder << "first:before"
+        request.setAttribute('firstInterceptorRan', true)
+        true
+    }
+
+    boolean after() {
+        InterceptorTestController.executionOrder << "first:after"
+        true
+    }
+
+    void afterView() {
+        InterceptorTestController.executionOrder << "first:afterView"
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/InterceptorTestController.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/InterceptorTestController.groovy
new file mode 100644
index 0000000000..3f520f68d9
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/InterceptorTestController.groovy
@@ -0,0 +1,172 @@
+/*
+ *  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.interceptors
+
+import grails.converters.JSON
+
+/**
+ * Controller for testing advanced interceptor functionality.
+ */
+class InterceptorTestController {
+
+    // This list tracks interceptor execution order
+    static List<String> executionOrder = []
+
+    /**
+     * Simple action for testing interceptor before/after execution.
+     */
+    def index() {
+        executionOrder << "controller:index"
+        render([
+            action: 'index',
+            executionOrder: new ArrayList<>(executionOrder)
+        ] as JSON)
+    }
+
+    /**
+     * Action for testing interceptor ordering.
+     */
+    def testOrder() {
+        executionOrder << "controller:testOrder"
+        render([
+            action: 'testOrder',
+            executionOrder: new ArrayList<>(executionOrder)
+        ] as JSON)
+    }
+
+    /**
+     * Action that can be blocked by interceptor.
+     */
+    def blocked() {
+        executionOrder << "controller:blocked"
+        render([
+            action: 'blocked',
+            message: 'This should not be seen if blocked'
+        ] as JSON)
+    }
+
+    /**
+     * Action that can throw exception.
+     */
+    def throwException() {
+        if (params.throwIt == 'true') {
+            throw new RuntimeException("Controller threw exception: 
${params.message ?: 'test error'}")
+        }
+        render([
+            action: 'throwException',
+            didNotThrow: true
+        ] as JSON)
+    }
+
+    /**
+     * Action for testing model modification by interceptor.
+     */
+    def modifyModel() {
+        executionOrder << "controller:modifyModel"
+        [
+            originalValue: 'from controller',
+            timestamp: System.currentTimeMillis()
+        ]
+    }
+
+    /**
+     * Action that returns data to be potentially modified by after 
interceptor.
+     */
+    def dataAction() {
+        executionOrder << "controller:dataAction"
+        render([
+            data: params.data ?: 'default',
+            interceptorModified: false
+        ] as JSON)
+    }
+
+    /**
+     * Reset execution order (for test setup).
+     */
+    def resetOrder() {
+        executionOrder.clear()
+        render([
+            reset: true,
+            executionOrder: executionOrder
+        ] as JSON)
+    }
+
+    /**
+     * Get current execution order without modifying it.
+     */
+    def getOrder() {
+        render([
+            executionOrder: new ArrayList<>(executionOrder)
+        ] as JSON)
+    }
+
+    /**
+     * Action for testing request attribute passing.
+     */
+    def checkAttributes() {
+        render([
+            interceptorSet: request.getAttribute('interceptorData'),
+            headerAdded: response.getHeader('X-Interceptor-Header'),
+            fromBefore: request.getAttribute('beforeInterceptorRan')
+        ] as JSON)
+    }
+
+    /**
+     * Action for testing session manipulation by interceptor.
+     */
+    def checkSession() {
+        render([
+            sessionData: session.getAttribute('interceptorSessionData'),
+            sessionId: session.id
+        ] as JSON)
+    }
+
+    /**
+     * Action for testing afterView interceptor.
+     */
+    def withView() {
+        executionOrder << "controller:withView"
+        [message: 'Hello from controller']
+    }
+
+    /**
+     * Action that takes time (for timing tests).
+     */
+    def slowAction() {
+        def delay = params.int('delay') ?: 100
+        Thread.sleep(delay)
+        executionOrder << "controller:slowAction"
+        render([
+            action: 'slowAction',
+            delay: delay
+        ] as JSON)
+    }
+
+    /**
+     * Action for testing conditional interceptor matching.
+     */
+    def conditionalAction() {
+        executionOrder << "controller:conditionalAction"
+        render([
+            action: 'conditionalAction',
+            param: params.match
+        ] as JSON)
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SecondInterceptor.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SecondInterceptor.groovy
new file mode 100644
index 0000000000..f6c70cfa74
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SecondInterceptor.groovy
@@ -0,0 +1,48 @@
+/*
+ *  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.interceptors
+
+/**
+ * Second interceptor in the chain (order = 20).
+ * Runs after FirstInterceptor but before ThirdInterceptor.
+ */
+class SecondInterceptor {
+
+    int order = 20
+
+    SecondInterceptor() {
+        match(controller: 'interceptorTest', action: 
~/(index|testOrder|dataAction)/)
+    }
+
+    boolean before() {
+        InterceptorTestController.executionOrder << "second:before"
+        request.setAttribute('secondInterceptorRan', true)
+        true
+    }
+
+    boolean after() {
+        InterceptorTestController.executionOrder << "second:after"
+        true
+    }
+
+    void afterView() {
+        InterceptorTestController.executionOrder << "second:afterView"
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SessionInterceptor.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SessionInterceptor.groovy
new file mode 100644
index 0000000000..129d85ad90
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/SessionInterceptor.groovy
@@ -0,0 +1,44 @@
+/*
+ *  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.interceptors
+
+/**
+ * Interceptor that sets session attributes.
+ */
+class SessionInterceptor {
+
+    SessionInterceptor() {
+        match(controller: 'interceptorTest', action: 'checkSession')
+    }
+
+    boolean before() {
+        InterceptorTestController.executionOrder << "session:before"
+        session.setAttribute('interceptorSessionData', [
+            setAt: System.currentTimeMillis(),
+            message: 'Session data from interceptor'
+        ])
+        true
+    }
+
+    boolean after() {
+        InterceptorTestController.executionOrder << "session:after"
+        true
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ThirdInterceptor.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ThirdInterceptor.groovy
new file mode 100644
index 0000000000..8bdbbaa47e
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/ThirdInterceptor.groovy
@@ -0,0 +1,48 @@
+/*
+ *  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.interceptors
+
+/**
+ * Third interceptor in the chain (order = 30).
+ * Runs last among the ordered interceptors.
+ */
+class ThirdInterceptor {
+
+    int order = 30
+
+    ThirdInterceptor() {
+        match(controller: 'interceptorTest', action: 
~/(index|testOrder|dataAction)/)
+    }
+
+    boolean before() {
+        InterceptorTestController.executionOrder << "third:before"
+        request.setAttribute('thirdInterceptorRan', true)
+        true
+    }
+
+    boolean after() {
+        InterceptorTestController.executionOrder << "third:after"
+        true
+    }
+
+    void afterView() {
+        InterceptorTestController.executionOrder << "third:afterView"
+    }
+}
diff --git 
a/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/TimingInterceptor.groovy
 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/TimingInterceptor.groovy
new file mode 100644
index 0000000000..8a4c2311e4
--- /dev/null
+++ 
b/grails-test-examples/app1/grails-app/controllers/functionaltests/interceptors/TimingInterceptor.groovy
@@ -0,0 +1,46 @@
+/*
+ *  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.interceptors
+
+/**
+ * Interceptor for timing/performance tracking.
+ */
+class TimingInterceptor {
+
+    TimingInterceptor() {
+        match(controller: 'interceptorTest', action: 'slowAction')
+    }
+
+    boolean before() {
+        InterceptorTestController.executionOrder << "timing:before"
+        request.setAttribute('requestStartTime', System.currentTimeMillis())
+        true
+    }
+
+    boolean after() {
+        def startTime = request.getAttribute('requestStartTime') as Long
+        def endTime = System.currentTimeMillis()
+        def duration = endTime - startTime
+        InterceptorTestController.executionOrder << 
"timing:after:${duration}ms"
+        // Add header without checking if committed
+        response.addHeader('X-Request-Duration', "${duration}")
+        true
+    }
+}
diff --git 
a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy
 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy
new file mode 100644
index 0000000000..9d5b028330
--- /dev/null
+++ 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy
@@ -0,0 +1,390 @@
+/*
+ *  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.async
+
+import functionaltests.Application
+import functionaltests.services.AsyncProcessingService
+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
+
+import java.util.concurrent.TimeUnit
+
+/**
+ * Integration tests for async/promise functionality in Grails.
+ * Tests various async patterns including tasks, promises, chaining, and error 
handling.
+ */
+@Integration(applicationClass = Application)
+@Rollback
+class AsyncPromiseSpec extends Specification {
+
+    @Shared
+    HttpClient client
+
+    AsyncProcessingService asyncProcessingService
+
+    def setup() {
+        client = HttpClient.create(new URL("http://localhost:${serverPort}";))
+    }
+
+    def cleanup() {
+        client?.close()
+    }
+
+    // ========== Basic Async Task Tests ==========
+
+    def "simple async task completes successfully"() {
+        when: "calling simple task endpoint"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/asyncTest/simpleTask'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "task completes with success status"
+        response.status == HttpStatus.OK
+        json.status == 'completed'
+        json.message == 'Task finished'
+    }
+
+    def "compute task returns calculated value"() {
+        given: "an input value"
+        def value = 7
+
+        when: "calling compute endpoint"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET("/asyncTest/computeTask?value=${value}"),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "computed result is correct"
+        response.status == HttpStatus.OK
+        json.input == value
+        json.result == value * value
+    }
+
+    def "parallel tasks complete and return all results"() {
+        when: "calling parallel tasks endpoint"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/asyncTest/parallelTasks'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "all tasks complete"
+        response.status == HttpStatus.OK
+        json.status == 'completed'
+        json.results.size() == 3
+        json.results.contains('Task 1 result')
+        json.results.contains('Task 2 result')
+        json.results.contains('Task 3 result')
+    }
+
+    def "chained tasks process data through multiple stages"() {
+        given: "an input string"
+        def input = 'hello'
+
+        when: "calling chained endpoint"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET("/asyncTest/chainedTasks?input=${input}"),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "data is processed through all stages"
+        response.status == HttpStatus.OK
+        json.original == input
+        // HELLO reversed is OLLEH
+        json.final == input.toUpperCase().reverse()
+    }
+
+    // ========== Error Handling Tests ==========
+
+    def "async task handles success without error"() {
+        when: "calling task that should succeed"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/asyncTest/taskWithError?fail=false'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "success response returned"
+        response.status == HttpStatus.OK
+        json.status == 'success'
+        json.result == 'Success'
+    }
+
+    def "async task with timeout completes within time limit"() {
+        when: "calling task with reasonable timeout"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            
HttpRequest.GET('/asyncTest/taskWithTimeout?delay=100&timeout=500'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "task completes successfully"
+        response.status == HttpStatus.OK
+        json.status == 'completed'
+        json.result == 'Completed in time'
+        json.elapsedMs >= 100
+    }
+
+    // ========== Async Service Tests ==========
+
+    def "async service processes string input"() {
+        given: "an input string"
+        def input = 'test'
+
+        when: "calling async service endpoint"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET("/asyncTest/useAsyncService?input=${input}"),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "service processes input correctly"
+        response.status == HttpStatus.OK
+        json.input == input
+        json.result == "Processed: ${input.toUpperCase()}"
+    }
+
+    def "async service calculates square"() {
+        given: "a numeric value"
+        def value = 6
+
+        when: "calling async calculation endpoint"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET("/asyncTest/asyncCalculation?value=${value}"),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "calculation is correct"
+        response.status == HttpStatus.OK
+        json.input == value
+        json.squared == value * value
+    }
+
+    def "async batch processing reverses all items"() {
+        when: "calling batch endpoint with default items"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/asyncTest/asyncBatch'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "all items are reversed"
+        response.status == HttpStatus.OK
+        json.original.size() == json.processed.size()
+        json.original.eachWithIndex { item, idx ->
+            assert json.processed[idx] == item.reverse()
+        }
+    }
+
+    def "long running operation completes with task info"() {
+        given: "a task ID"
+        def taskId = 'task-123'
+
+        when: "calling long running endpoint"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET("/asyncTest/longRunning?taskId=${taskId}"),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "operation completes with expected info"
+        response.status == HttpStatus.OK
+        json.taskId == taskId
+        json.status == 'completed'
+        json.durationMs >= 200
+        json.completedAt != null
+    }
+
+    def "CompletableFuture composition combines results"() {
+        given: "two input values"
+        def v1 = 3
+        def v2 = 4
+
+        when: "calling compose endpoint"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET("/asyncTest/composeFutures?v1=${v1}&v2=${v2}"),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "both futures are combined correctly"
+        response.status == HttpStatus.OK
+        json.value1Squared == v1 * v1  // 9
+        json.value2Squared == v2 * v2  // 16
+        json.sum == (v1 * v1) + (v2 * v2)  // 25
+    }
+
+    // ========== Request Data Processing Tests ==========
+
+    def "async task processes JSON request body"() {
+        given: "JSON request body"
+        def body = '{"name": "test", "value": "hello"}'
+
+        when: "posting to process endpoint"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.POST('/asyncTest/processRequestData', body)
+                .contentType(MediaType.APPLICATION_JSON),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "data is processed correctly"
+        response.status == HttpStatus.OK
+        json.original.name == 'test'
+        json.processed.name == 'TEST'
+        json.processed.value == 'HELLO'
+    }
+
+    def "multi-stage process reports all stages"() {
+        when: "calling multi-stage endpoint"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/asyncTest/multiStageProcess'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "all stages are reported"
+        response.status == HttpStatus.OK
+        json.status == 'completed'
+        json.totalStages == 3
+        json.stages.size() == 3
+        json.stages[0].action == 'initialize'
+        json.stages[1].action == 'process'
+        json.stages[2].action == 'finalize'
+    }
+
+    def "stages execute in correct order"() {
+        when: "calling multi-stage endpoint"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            HttpRequest.GET('/asyncTest/multiStageProcess'),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "stages have increasing timestamps"
+        response.status == HttpStatus.OK
+        def times = json.stages.collect { it.time as Long }
+        times[0] <= times[1]
+        times[1] <= times[2]
+    }
+
+    // ========== Conditional Execution Tests ==========
+
+    def "conditional async uses async mode when requested"() {
+        given: "input value"
+        def input = 'grails'
+
+        when: "calling with async=true"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            
HttpRequest.GET("/asyncTest/conditionalAsync?async=true&input=${input}"),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "async mode is used"
+        response.status == HttpStatus.OK
+        json.mode == 'async'
+        json.result.contains('Async')
+        json.result.contains(input.toUpperCase())
+    }
+
+    def "conditional async uses sync mode when requested"() {
+        given: "input value"
+        def input = 'grails'
+
+        when: "calling with async=false"
+        HttpResponse<String> response = client.toBlocking().exchange(
+            
HttpRequest.GET("/asyncTest/conditionalAsync?async=false&input=${input}"),
+            String
+        )
+        def json = new JsonSlurper().parseText(response.body())
+
+        then: "sync mode is used"
+        response.status == HttpStatus.OK
+        json.mode == 'sync'
+        json.result.contains('Sync')
+        json.result.contains(input.toUpperCase())
+    }
+
+    // ========== Direct Service Tests ==========
+
+    def "asyncProcessingService.processAsync returns correct result"() {
+        given: "an input"
+        def input = 'direct'
+
+        when: "calling service directly"
+        def future = asyncProcessingService.processAsync(input)
+        def result = future.get(5, TimeUnit.SECONDS)
+
+        then: "result is correct"
+        result == "Processed: ${input.toUpperCase()}"
+    }
+
+    def "asyncProcessingService.calculateAsync squares value"() {
+        given: "a numeric value"
+        def value = 8
+
+        when: "calling calculation service"
+        def future = asyncProcessingService.calculateAsync(value)
+        def result = future.get(5, TimeUnit.SECONDS)
+
+        then: "value is squared"
+        result == value * value
+    }
+
+    def "asyncProcessingService.processBatchAsync handles empty list"() {
+        given: "empty list"
+        def items = []
+
+        when: "processing empty batch"
+        def future = asyncProcessingService.processBatchAsync(items)
+        def result = future.get(5, TimeUnit.SECONDS)
+
+        then: "empty result returned"
+        result != null
+        result.isEmpty()
+    }
+
+    def "asyncProcessingService.processBatchAsync handles single item"() {
+        given: "single item list"
+        def items = ['single']
+
+        when: "processing single item batch"
+        def future = asyncProcessingService.processBatchAsync(items)
+        def result = future.get(5, TimeUnit.SECONDS)
+
+        then: "single reversed item returned"
+        result.size() == 1
+        result[0] == 'elgnis'
+    }
+}
diff --git 
a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/interceptors/InterceptorOrderingSpec.groovy
 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/interceptors/InterceptorOrderingSpec.groovy
new file mode 100644
index 0000000000..da64200df5
--- /dev/null
+++ 
b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/interceptors/InterceptorOrderingSpec.groovy
@@ -0,0 +1,407 @@
+/*
+ *  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.interceptors
+
+import functionaltests.Application
+import grails.testing.mixin.integration.Integration
+import io.micronaut.http.HttpRequest
+import io.micronaut.http.client.HttpClient
+import spock.lang.Specification
+
+/**
+ * Comprehensive integration tests for Grails interceptor functionality.
+ * 
+ * Tests cover:
+ * - Interceptor execution order (before/after/afterView)
+ * - Interceptor ordering using 'order' property
+ * - Blocking requests in before()
+ * - Request/response attribute manipulation
+ * - Session manipulation
+ * - Conditional matching
+ * - Timing/performance tracking
+ */
+@Integration(applicationClass = Application)
+class InterceptorOrderingSpec extends Specification {
+
+    private HttpClient createClient() {
+        HttpClient.create(new URL("http://localhost:$serverPort";))
+    }
+
+    def setup() {
+        // Reset execution order before each test
+        def client = createClient()
+        try {
+            client.toBlocking().exchange(
+                HttpRequest.GET('/interceptorTest/resetOrder'),
+                Map
+            )
+        } finally {
+            client.close()
+        }
+    }
+
+    // ========== Interceptor Ordering Tests ==========
+
+    def "test interceptors execute in order by 'order' property"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/testOrder'),
+            Map
+        )
+
+        then: "interceptors should run in order: first(10), second(20), 
third(30)"
+        response.status.code == 200
+        def order = response.body().executionOrder
+        
+        // Before interceptors run in ascending order
+        def firstBeforeIdx = order.indexOf('first:before')
+        def secondBeforeIdx = order.indexOf('second:before')
+        def thirdBeforeIdx = order.indexOf('third:before')
+        def controllerIdx = order.indexOf('controller:testOrder')
+        
+        // Verify before interceptors run before controller
+        firstBeforeIdx >= 0
+        secondBeforeIdx >= 0
+        thirdBeforeIdx >= 0
+        controllerIdx >= 0
+        
+        firstBeforeIdx < secondBeforeIdx
+        secondBeforeIdx < thirdBeforeIdx
+        thirdBeforeIdx < controllerIdx
+
+        cleanup:
+        client.close()
+    }
+
+    def "test before interceptors run before controller action"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/index'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        def order = response.body().executionOrder
+        
+        // All before interceptors should run before controller
+        order.findAll { it.contains(':before') }.every { beforeEntry ->
+            order.indexOf(beforeEntry) < order.indexOf('controller:index')
+        }
+
+        cleanup:
+        client.close()
+    }
+
+    def "test after interceptors run after controller action"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/index'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        def order = response.body().executionOrder
+        
+        // All after interceptors should run after controller
+        order.findAll { it.contains(':after') }.every { afterEntry ->
+            order.indexOf(afterEntry) > order.indexOf('controller:index')
+        }
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Blocking Interceptor Tests ==========
+
+    def "test interceptor can block request by returning false"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            
HttpRequest.GET('/interceptorTest/blocked?block=true&reason=testing'),
+            Map
+        )
+
+        then: "controller action should not execute"
+        response.status.code == 200
+        response.body().blocked == true
+        response.body().message == 'Request blocked by interceptor'
+        response.body().reason == 'testing'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test interceptor allows request when returning true"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/blocked?block=false'),
+            Map
+        )
+
+        then: "controller action should execute"
+        response.status.code == 200
+        response.body().action == 'blocked'
+        response.body().message == 'This should not be seen if blocked'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Request Attribute Tests ==========
+
+    def "test interceptor can set request attributes"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/checkAttributes'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().fromBefore == true
+        response.body().interceptorSet != null
+        response.body().interceptorSet.source == 'AttributeSettingInterceptor'
+
+        cleanup:
+        client.close()
+    }
+
+    def "test interceptor can set response headers"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/checkAttributes'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.header('X-Interceptor-Header') == 'set-by-interceptor'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Session Tests ==========
+
+    def "test interceptor can set session attributes"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/checkSession'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().sessionData != null
+        response.body().sessionData.message == 'Session data from interceptor'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Conditional Matching Tests ==========
+
+    def "test interceptor conditional matching - matched"() {
+        given:
+        def client = createClient()
+        // Reset first
+        
client.toBlocking().exchange(HttpRequest.GET('/interceptorTest/resetOrder'), 
Map)
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/conditionalAction?match=yes'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        def orderResponse = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/getOrder'),
+            Map
+        )
+        
orderResponse.body().executionOrder.contains('conditional:before:matched')
+
+        cleanup:
+        client.close()
+    }
+
+    def "test interceptor conditional matching - not matched"() {
+        given:
+        def client = createClient()
+        // Reset first
+        
client.toBlocking().exchange(HttpRequest.GET('/interceptorTest/resetOrder'), 
Map)
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/conditionalAction?match=no'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        def orderResponse = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/getOrder'),
+            Map
+        )
+        
orderResponse.body().executionOrder.contains('conditional:before:notmatched')
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Timing Interceptor Tests ==========
+
+    def "test timing interceptor tracks request duration"() {
+        given:
+        def client = createClient()
+        // Reset first
+        
client.toBlocking().exchange(HttpRequest.GET('/interceptorTest/resetOrder'), 
Map)
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/slowAction?delay=50'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().action == 'slowAction'
+        response.body().delay == 50
+        
+        // Check execution order includes timing entries
+        def orderResponse = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/getOrder'),
+            Map
+        )
+        orderResponse.body().executionOrder.any { 
it.startsWith('timing:before') }
+        orderResponse.body().executionOrder.any { 
it.startsWith('timing:after') }
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Multiple Requests Tests ==========
+
+    def "test interceptors work correctly across multiple requests"() {
+        given:
+        def client = createClient()
+
+        when: "make multiple requests"
+        
client.toBlocking().exchange(HttpRequest.GET('/interceptorTest/resetOrder'), 
Map)
+        def response1 = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/index'),
+            Map
+        )
+        
+        // Reset again for clean second request
+        
client.toBlocking().exchange(HttpRequest.GET('/interceptorTest/resetOrder'), 
Map)
+        def response2 = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/index'),
+            Map
+        )
+
+        then: "each request should have clean interceptor execution"
+        response1.status.code == 200
+        response2.status.code == 200
+        
+        // Both should have similar execution patterns
+        response1.body().executionOrder.contains('first:before')
+        response2.body().executionOrder.contains('first:before')
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Data Action Tests ==========
+
+    def "test interceptors work with data actions"() {
+        given:
+        def client = createClient()
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/dataAction?data=testValue'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        response.body().data == 'testValue'
+
+        cleanup:
+        client.close()
+    }
+
+    // ========== Execution Order Verification ==========
+
+    def "test complete before-controller-after sequence"() {
+        given:
+        def client = createClient()
+        
client.toBlocking().exchange(HttpRequest.GET('/interceptorTest/resetOrder'), 
Map)
+
+        when:
+        def response = client.toBlocking().exchange(
+            HttpRequest.GET('/interceptorTest/index'),
+            Map
+        )
+
+        then:
+        response.status.code == 200
+        def order = response.body().executionOrder
+        
+        // Verify the complete sequence
+        def beforeEntries = order.findAll { it.contains(':before') }
+        def afterEntries = order.findAll { it.contains(':after') }
+        def controllerEntry = order.find { it.startsWith('controller:') }
+        
+        // All befores come first
+        beforeEntries.every { order.indexOf(it) < 
order.indexOf(controllerEntry) }
+        // All afters come last
+        afterEntries.every { order.indexOf(it) > 
order.indexOf(controllerEntry) }
+
+        cleanup:
+        client.close()
+    }
+}

Reply via email to