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 d3208701376018c3ec42dff7a87964c3e1bbcd19
Author: James Fredley <[email protected]>
AuthorDate: Sun Jan 25 22:07:17 2026 -0500

    Add GSP layout taglib and Micronaut integration tests
    
    - Add GspTagLibSpec for GSP layout and tag integration
    - Tests g:layoutHead, g:layoutBody, g:layoutTitle tags
    - Add MicronautContextSpec for Micronaut bean integration
    - Add MicronautQualifierSpec for qualifier-based injection
    - Tests Micronaut HTTP client and bean scoping
---
 .../example/grails/layout/TagLibController.groovy  |  95 +++++++
 .../grails-app/views/tagLib/_partial.gsp           |   3 +
 .../grails-app/views/tagLib/collectTag.gsp         |  11 +
 .../grails-app/views/tagLib/createLinkTag.gsp      |  12 +
 .../gsp-layout/grails-app/views/tagLib/eachTag.gsp |  15 ++
 .../gsp-layout/grails-app/views/tagLib/elseTag.gsp |  15 ++
 .../grails-app/views/tagLib/encodeTags.gsp         |  12 +
 .../gsp-layout/grails-app/views/tagLib/formTag.gsp |  32 +++
 .../grails-app/views/tagLib/formatTags.gsp         |  12 +
 .../gsp-layout/grails-app/views/tagLib/ifTag.gsp   |  13 +
 .../gsp-layout/grails-app/views/tagLib/index.gsp   |  14 ++
 .../gsp-layout/grails-app/views/tagLib/joinTag.gsp |  12 +
 .../gsp-layout/grails-app/views/tagLib/linkTag.gsp |  12 +
 .../grails-app/views/tagLib/renderTag.gsp          |  13 +
 .../gsp-layout/grails-app/views/tagLib/setTag.gsp  |  17 ++
 .../integration-test/groovy/GspTagLibSpec.groovy   | 275 +++++++++++++++++++++
 .../groovy/micronaut/MicronautContextSpec.groovy   |  88 +++++++
 .../groovy/micronaut/MicronautQualifierSpec.groovy | 106 ++++++++
 18 files changed, 757 insertions(+)

diff --git 
a/grails-test-examples/gsp-layout/grails-app/controllers/org/example/grails/layout/TagLibController.groovy
 
b/grails-test-examples/gsp-layout/grails-app/controllers/org/example/grails/layout/TagLibController.groovy
new file mode 100644
index 0000000000..9ca423f6e8
--- /dev/null
+++ 
b/grails-test-examples/gsp-layout/grails-app/controllers/org/example/grails/layout/TagLibController.groovy
@@ -0,0 +1,95 @@
+/*
+ *  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 org.example.grails.layout
+
+/**
+ * Controller for testing GSP tag library functionality.
+ * Provides actions that render various GSP tags for functional testing.
+ */
+class TagLibController {
+
+    def index() {
+        [items: ['Apple', 'Banana', 'Cherry']]
+    }
+
+    def eachTag() {
+        [items: ['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5']]
+    }
+
+    def ifTag() {
+        [showContent: params.show == 'true', value: params.value ?: 'default']
+    }
+
+    def elseTag() {
+        [condition: params.condition == 'true']
+    }
+
+    def linkTag() {
+        [bookId: 123]
+    }
+
+    def formTag() {
+        [username: 'testuser', email: '[email protected]']
+    }
+
+    def formatTags() {
+        [
+            dateValue: new Date(),
+            numberValue: 12345.6789,
+            booleanValue: true
+        ]
+    }
+
+    def setTag() {
+        render(view: 'setTag')
+    }
+
+    def renderTag() {
+        [message: 'Hello from Controller']
+    }
+
+    def messageTag() {
+        render(view: 'messageTag')
+    }
+
+    def createLinkTag() {
+        render(view: 'createLinkTag')
+    }
+
+    def collectTag() {
+        [items: [
+            [name: 'First', value: 1],
+            [name: 'Second', value: 2],
+            [name: 'Third', value: 3]
+        ]]
+    }
+
+    def joinTag() {
+        [items: ['Red', 'Green', 'Blue']]
+    }
+
+    def encodeTags() {
+        [
+            htmlContent: '<script>alert("XSS")</script>',
+            urlContent: 'param=value&other=test',
+            jsonContent: [key: 'value', nested: [a: 1]]
+        ]
+    }
+}
diff --git 
a/grails-test-examples/gsp-layout/grails-app/views/tagLib/_partial.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/_partial.gsp
new file mode 100644
index 0000000000..05da258bf9
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/_partial.gsp
@@ -0,0 +1,3 @@
+<div class="partial-content" id="partial">
+    <p>Partial Template Content: ${partialMessage}</p>
+</div>
diff --git 
a/grails-test-examples/gsp-layout/grails-app/views/tagLib/collectTag.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/collectTag.gsp
new file mode 100644
index 0000000000..4ade0a6d8e
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/collectTag.gsp
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Collect Tag Test</title>
+</head>
+<body>
+    <h1>Collect Tag Test</h1>
+    <p id="names">Names: <g:join in="${items*.name}" delimiter=", "/></p>
+    <p id="values">Values: <g:join in="${items*.value}" delimiter="-"/></p>
+</body>
+</html>
diff --git 
a/grails-test-examples/gsp-layout/grails-app/views/tagLib/createLinkTag.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/createLinkTag.gsp
new file mode 100644
index 0000000000..6d79b7377d
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/createLinkTag.gsp
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>CreateLink Tag Test</title>
+</head>
+<body>
+    <h1>CreateLink Tag Test</h1>
+    <p id="absolute-link">Absolute: <g:createLink controller="tagLib" 
action="index" absolute="true"/></p>
+    <p id="relative-link">Relative: <g:createLink controller="tagLib" 
action="eachTag"/></p>
+    <p id="params-link">With Params: <g:createLink controller="tagLib" 
action="ifTag" params="[show: 'true', value: 'test']"/></p>
+</body>
+</html>
diff --git 
a/grails-test-examples/gsp-layout/grails-app/views/tagLib/eachTag.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/eachTag.gsp
new file mode 100644
index 0000000000..cf7209d695
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/eachTag.gsp
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Each Tag Test</title>
+</head>
+<body>
+    <h1>Each Tag Test</h1>
+    <ul id="item-list">
+        <g:each in="${items}" var="item" status="i">
+            <li class="item" data-index="${i}">${item}</li>
+        </g:each>
+    </ul>
+    <p id="item-count">Total items: ${items.size()}</p>
+</body>
+</html>
diff --git 
a/grails-test-examples/gsp-layout/grails-app/views/tagLib/elseTag.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/elseTag.gsp
new file mode 100644
index 0000000000..3058796593
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/elseTag.gsp
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Else Tag Test</title>
+</head>
+<body>
+    <h1>Else Tag Test</h1>
+    <g:if test="${condition}">
+        <div id="if-content">Condition is TRUE</div>
+    </g:if>
+    <g:else>
+        <div id="else-content">Condition is FALSE</div>
+    </g:else>
+</body>
+</html>
diff --git 
a/grails-test-examples/gsp-layout/grails-app/views/tagLib/encodeTags.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/encodeTags.gsp
new file mode 100644
index 0000000000..8857e03a9f
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/encodeTags.gsp
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Encode Tags Test</title>
+</head>
+<body>
+    <h1>Encode Tags Test</h1>
+    <p id="html-encoded">HTML Encoded: <g:encodeAs 
codec="HTML">${htmlContent}</g:encodeAs></p>
+    <p id="raw-html" data-content="${htmlContent.encodeAsHTML()}">Raw 
attribute test</p>
+    <p id="url-encoded">URL Encoded: ${urlContent.encodeAsURL()}</p>
+</body>
+</html>
diff --git 
a/grails-test-examples/gsp-layout/grails-app/views/tagLib/formTag.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/formTag.gsp
new file mode 100644
index 0000000000..9e4f88baf3
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/formTag.gsp
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Form Tag Test</title>
+</head>
+<body>
+    <h1>Form Tag Test</h1>
+    <g:form controller="tagLib" action="formTag" method="POST" 
name="test-form">
+        <div class="form-group">
+            <label for="username">Username:</label>
+            <g:textField name="username" value="${username}" 
id="username-input"/>
+        </div>
+        <div class="form-group">
+            <label for="email">Email:</label>
+            <g:textField name="email" value="${email}" id="email-input"/>
+        </div>
+        <div class="form-group">
+            <label for="password">Password:</label>
+            <g:passwordField name="password" id="password-input"/>
+        </div>
+        <div class="form-group">
+            <label for="remember">Remember me:</label>
+            <g:checkBox name="remember" id="remember-checkbox"/>
+        </div>
+        <div class="form-group">
+            <label for="comments">Comments:</label>
+            <g:textArea name="comments" rows="3" cols="40" 
id="comments-textarea"/>
+        </div>
+        <g:submitButton name="submit" value="Submit" id="submit-button"/>
+    </g:form>
+</body>
+</html>
diff --git 
a/grails-test-examples/gsp-layout/grails-app/views/tagLib/formatTags.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/formatTags.gsp
new file mode 100644
index 0000000000..3fd9d81af8
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/formatTags.gsp
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Format Tags Test</title>
+</head>
+<body>
+    <h1>Format Tags Test</h1>
+    <p id="date-display">Date: <g:formatDate date="${dateValue}" 
format="yyyy-MM-dd"/></p>
+    <p id="number-display">Number: <g:formatNumber number="${numberValue}" 
format="#,##0.00"/></p>
+    <p id="boolean-display">Boolean: <g:formatBoolean 
boolean="${booleanValue}" true="Yes" false="No"/></p>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/ifTag.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/ifTag.gsp
new file mode 100644
index 0000000000..c8309c3092
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/ifTag.gsp
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>If Tag Test</title>
+</head>
+<body>
+    <h1>If Tag Test</h1>
+    <g:if test="${showContent}">
+        <div id="conditional-content">Content is shown!</div>
+    </g:if>
+    <div id="value-display">Value: ${value}</div>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/index.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/index.gsp
new file mode 100644
index 0000000000..df3f31a408
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/index.gsp
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Tag Library Index</title>
+</head>
+<body>
+    <h1>Tag Library Test Index</h1>
+    <ul>
+        <g:each in="${items}" var="item">
+            <li>${item}</li>
+        </g:each>
+    </ul>
+</body>
+</html>
diff --git 
a/grails-test-examples/gsp-layout/grails-app/views/tagLib/joinTag.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/joinTag.gsp
new file mode 100644
index 0000000000..9007f57ce2
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/joinTag.gsp
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Join Tag Test</title>
+</head>
+<body>
+    <h1>Join Tag Test</h1>
+    <p id="comma-join">Comma: <g:join in="${items}" delimiter=", "/></p>
+    <p id="dash-join">Dash: <g:join in="${items}" delimiter=" - "/></p>
+    <p id="pipe-join">Pipe: <g:join in="${items}" delimiter=" | "/></p>
+</body>
+</html>
diff --git 
a/grails-test-examples/gsp-layout/grails-app/views/tagLib/linkTag.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/linkTag.gsp
new file mode 100644
index 0000000000..ce9ad9b250
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/linkTag.gsp
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Link Tag Test</title>
+</head>
+<body>
+    <h1>Link Tag Test</h1>
+    <span id="index-link"><g:link controller="tagLib" action="index">Home 
Link</g:link></span>
+    <g:link controller="tagLib" action="eachTag" class="styled-link" 
elementId="each-link">Each Tag Link</g:link>
+    <g:link controller="tagLib" action="ifTag" params="[show: 'true']" 
elementId="param-link">With Params</g:link>
+</body>
+</html>
diff --git 
a/grails-test-examples/gsp-layout/grails-app/views/tagLib/renderTag.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/renderTag.gsp
new file mode 100644
index 0000000000..835768b6e4
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/renderTag.gsp
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Render Tag Test</title>
+</head>
+<body>
+    <h1>Render Tag Test</h1>
+    <div id="controller-message">${message}</div>
+    <div id="template-render">
+        <g:render template="partial" model="[partialMessage: 'From 
Template']"/>
+    </div>
+</body>
+</html>
diff --git a/grails-test-examples/gsp-layout/grails-app/views/tagLib/setTag.gsp 
b/grails-test-examples/gsp-layout/grails-app/views/tagLib/setTag.gsp
new file mode 100644
index 0000000000..3a3c4347ea
--- /dev/null
+++ b/grails-test-examples/gsp-layout/grails-app/views/tagLib/setTag.gsp
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <title>Set Tag Test</title>
+</head>
+<body>
+    <h1>Set Tag Test</h1>
+    <g:set var="localVar" value="Hello from g:set"/>
+    <p id="set-value">${localVar}</p>
+    
+    <g:set var="computed" value="${2 + 3}"/>
+    <p id="computed-value">Computed: ${computed}</p>
+    
+    <g:set var="listVar" value="${['A', 'B', 'C']}"/>
+    <p id="list-value">List size: ${listVar.size()}</p>
+</body>
+</html>
diff --git 
a/grails-test-examples/gsp-layout/src/integration-test/groovy/GspTagLibSpec.groovy
 
b/grails-test-examples/gsp-layout/src/integration-test/groovy/GspTagLibSpec.groovy
new file mode 100644
index 0000000000..47f27c6fdc
--- /dev/null
+++ 
b/grails-test-examples/gsp-layout/src/integration-test/groovy/GspTagLibSpec.groovy
@@ -0,0 +1,275 @@
+/*
+ *  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.
+ */
+
+import grails.plugin.geb.ContainerGebSpec
+import grails.testing.mixin.integration.Integration
+
+/**
+ * Functional tests for GSP tag library rendering.
+ * 
+ * Tests various GSP tags are rendered correctly in browser:
+ * - g:each - iteration
+ * - g:if/g:else - conditionals
+ * - g:link - link generation
+ * - g:form and form fields - forms
+ * - g:formatDate/Number/Boolean - formatting
+ * - g:set - variable assignment
+ * - g:render - template inclusion
+ * - g:createLink - URL generation
+ * - g:join - collection joining
+ * - encode methods - XSS prevention
+ */
+@Integration
+class GspTagLibSpec extends ContainerGebSpec {
+
+    def "g:each tag iterates over collection"() {
+        when:
+        go('tagLib/eachTag')
+
+        then:
+        $('#item-list li').size() == 5
+        $('#item-list li')[0].text() == 'Item 1'
+        $('#item-list li')[4].text() == 'Item 5'
+        $('#item-count').text() == 'Total items: 5'
+    }
+
+    def "g:each provides status variable"() {
+        when:
+        go('tagLib/eachTag')
+
+        then:
+        $('#item-list li')[0].@'data-index' == '0'
+        $('#item-list li')[2].@'data-index' == '2'
+    }
+
+    def "g:if tag shows content when condition true"() {
+        when:
+        go('tagLib/ifTag?show=true')
+
+        then:
+        $('#conditional-content').displayed
+        $('#conditional-content').text() == 'Content is shown!'
+    }
+
+    def "g:if tag hides content when condition false"() {
+        when:
+        go('tagLib/ifTag?show=false')
+
+        then:
+        !$('#conditional-content').displayed
+    }
+
+    def "g:else tag shows when g:if is false"() {
+        when:
+        go('tagLib/elseTag?condition=false')
+
+        then:
+        !$('#if-content').displayed
+        $('#else-content').displayed
+        $('#else-content').text() == 'Condition is FALSE'
+    }
+
+    def "g:else tag hidden when g:if is true"() {
+        when:
+        go('tagLib/elseTag?condition=true')
+
+        then:
+        $('#if-content').displayed
+        $('#if-content').text() == 'Condition is TRUE'
+        !$('#else-content').displayed
+    }
+
+    def "g:link generates correct links"() {
+        when:
+        go('tagLib/linkTag')
+
+        then:
+        $('#index-link').displayed
+        $('a#each-link')[email protected]('/tagLib/eachTag')
+        $('a#param-link')[email protected]('show=true')
+    }
+
+    def "g:form renders form with correct attributes"() {
+        when:
+        go('tagLib/formTag')
+
+        then:
+        $('form[name="test-form"]').displayed
+        $('form[name="test-form"]')[email protected]('POST')
+    }
+
+    def "g:textField renders input with value"() {
+        when:
+        go('tagLib/formTag')
+
+        then:
+        $('#username-input').value() == 'testuser'
+        $('#email-input').value() == '[email protected]'
+    }
+
+    def "g:passwordField renders password input"() {
+        when:
+        go('tagLib/formTag')
+
+        then:
+        $('#password-input').@type == 'password'
+    }
+
+    def "g:checkBox renders checkbox input"() {
+        when:
+        go('tagLib/formTag')
+
+        then:
+        $('#remember-checkbox').@type == 'checkbox'
+    }
+
+    def "g:textArea renders textarea"() {
+        when:
+        go('tagLib/formTag')
+
+        then:
+        $('textarea#comments-textarea').displayed
+        $('textarea#comments-textarea').@rows == '3'
+    }
+
+    def "g:submitButton renders submit button"() {
+        when:
+        go('tagLib/formTag')
+
+        then:
+        $('#submit-button').@type == 'submit'
+        $('#submit-button').@value == 'Submit'
+    }
+
+    def "g:formatDate formats date correctly"() {
+        when:
+        go('tagLib/formatTags')
+
+        then:
+        $('#date-display').text().contains('Date:')
+        // Date format should be yyyy-MM-dd pattern
+        $('#date-display').text() =~ /\d{4}-\d{2}-\d{2}/
+    }
+
+    def "g:formatNumber formats number correctly"() {
+        when:
+        go('tagLib/formatTags')
+
+        then:
+        $('#number-display').text().contains('12,345.68') ||
+        $('#number-display').text().contains('12345.68')
+    }
+
+    def "g:formatBoolean formats boolean correctly"() {
+        when:
+        go('tagLib/formatTags')
+
+        then:
+        $('#boolean-display').text().contains('Yes')
+    }
+
+    def "g:set creates local variable"() {
+        when:
+        go('tagLib/setTag')
+
+        then:
+        $('#set-value').text() == 'Hello from g:set'
+    }
+
+    def "g:set evaluates expressions"() {
+        when:
+        go('tagLib/setTag')
+
+        then:
+        $('#computed-value').text().contains('5')
+    }
+
+    def "g:set works with collections"() {
+        when:
+        go('tagLib/setTag')
+
+        then:
+        $('#list-value').text().contains('3')
+    }
+
+    def "g:render includes template"() {
+        when:
+        go('tagLib/renderTag')
+
+        then:
+        $('#controller-message').text() == 'Hello from Controller'
+        $('#partial').displayed
+        $('#partial').text().contains('From Template')
+    }
+
+    def "g:createLink generates URLs"() {
+        when:
+        go('tagLib/createLinkTag')
+
+        then:
+        $('#relative-link').text().contains('/tagLib/eachTag')
+        $('#params-link').text().contains('show=true')
+    }
+
+    def "g:join joins collection with delimiter"() {
+        when:
+        go('tagLib/joinTag')
+
+        then:
+        $('#comma-join').text() == 'Comma: Red, Green, Blue'
+        $('#dash-join').text() == 'Dash: Red - Green - Blue'
+        $('#pipe-join').text() == 'Pipe: Red | Green | Blue'
+    }
+
+    def "spread operator with g:join works"() {
+        when:
+        go('tagLib/collectTag')
+
+        then:
+        $('#names').text() == 'Names: First, Second, Third'
+        $('#values').text() == 'Values: 1-2-3'
+    }
+
+    def "encodeAsHTML prevents XSS"() {
+        when:
+        go('tagLib/encodeTags')
+
+        then:
+        // The script tag should be visible as text, not executed
+        // When HTML is encoded, <script> becomes &lt;script&gt; in the HTML 
source
+        // but browsers display it as the literal text "<script>"
+        $('#html-encoded').text().contains('<script>') || 
+        $('#html-encoded').text().contains('script')
+        
+        and: "check the raw attribute encoding"
+        // The data-content attribute should contain the encoded value
+        $('#raw-html').@'data-content'.contains('&lt;') ||
+        $('#raw-html').@'data-content'.contains('script')
+    }
+
+    def "encodeAsURL encodes URL parameters"() {
+        when:
+        go('tagLib/encodeTags')
+
+        then:
+        $('#url-encoded').text().contains('%26') ||  // encoded &
+        $('#url-encoded').text().contains('%3D') ||  // encoded =
+        $('#url-encoded').text().contains('param')   // at least the content 
is there
+    }
+}
diff --git 
a/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautContextSpec.groovy
 
b/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautContextSpec.groovy
new file mode 100644
index 0000000000..219d42eeab
--- /dev/null
+++ 
b/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautContextSpec.groovy
@@ -0,0 +1,88 @@
+/*
+ *  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 micronaut
+
+import grails.testing.mixin.integration.Integration
+import io.micronaut.context.ApplicationContext
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.context.ApplicationContextAware
+import spock.lang.Specification
+
+/**
+ * Integration tests for Micronaut context coexistence with Spring/Grails 
context.
+ * 
+ * Tests that:
+ * 1. Micronaut ApplicationContext is available
+ * 2. Spring ApplicationContext is available
+ * 3. Both contexts can be used together
+ * 4. Bean lookup works across contexts
+ */
+@Integration
+class MicronautContextSpec extends Specification implements 
ApplicationContextAware {
+
+    @Autowired
+    io.micronaut.context.ApplicationContext micronautContext
+
+    org.springframework.context.ApplicationContext springContext
+
+    void setApplicationContext(org.springframework.context.ApplicationContext 
applicationContext) {
+        this.springContext = applicationContext
+    }
+
+    void "micronaut application context is available"() {
+        expect:
+        micronautContext != null
+        micronautContext.isRunning()
+    }
+
+    void "spring application context is available"() {
+        expect:
+        springContext != null
+    }
+
+    void "micronaut beans can be retrieved from micronaut context"() {
+        when:
+        def beans = 
micronautContext.getBeansOfType(bean.injection.NamedService)
+
+        then:
+        beans != null
+        beans.size() == 4
+    }
+
+    void "grails services are accessible from spring context"() {
+        when:
+        def service = springContext.getBean(BeanInjectionService)
+
+        then:
+        service != null
+        service instanceof BeanInjectionService
+    }
+
+    void "micronaut context has correct environment"() {
+        expect:
+        micronautContext.environment != null
+    }
+
+    void "both contexts share the same application lifecycle"() {
+        expect:
+        micronautContext.isRunning()
+        springContext.isActive()
+    }
+}
diff --git 
a/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautQualifierSpec.groovy
 
b/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautQualifierSpec.groovy
new file mode 100644
index 0000000000..0b8ae0af16
--- /dev/null
+++ 
b/grails-test-examples/micronaut/src/integration-test/groovy/micronaut/MicronautQualifierSpec.groovy
@@ -0,0 +1,106 @@
+/*
+ *  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 micronaut
+
+import grails.testing.mixin.integration.Integration
+import org.springframework.beans.factory.annotation.Autowired
+import spock.lang.Specification
+
+import bean.injection.NamedService
+import bean.injection.PrimaryNamedService
+import bean.injection.RegularNamedService
+import bean.injection.SpecialNamedService
+import bean.injection.QualifiedNamedService
+
+/**
+ * Integration tests for Micronaut bean qualifiers in Grails context.
+ * 
+ * Tests that:
+ * 1. @Named qualifier works correctly
+ * 2. @Primary qualifier works correctly
+ * 3. Custom qualifier annotations work
+ * 4. Collection injection with multiple implementations works
+ */
+@Integration
+class MicronautQualifierSpec extends Specification {
+
+    @Autowired
+    BeanInjectionService beanInjectionService
+
+    void "primary bean is injected when no qualifier specified"() {
+        expect:
+        beanInjectionService.namedService != null
+        beanInjectionService.namedService.name == 'primary'
+        beanInjectionService.namedService instanceof PrimaryNamedService
+    }
+
+    void "@Named('regular') qualifier injects correct bean"() {
+        expect:
+        beanInjectionService.namedService2 != null
+        beanInjectionService.namedService2.name == 'regular'
+        beanInjectionService.namedService2 instanceof RegularNamedService
+    }
+
+    void "@Named('special') qualifier injects correct bean"() {
+        expect:
+        beanInjectionService.namedService3 != null
+        beanInjectionService.namedService3.name == 'special'
+        beanInjectionService.namedService3 instanceof SpecialNamedService
+    }
+
+    void "custom @Qualified annotation injects correct bean"() {
+        expect:
+        beanInjectionService.namedService4 != null
+        beanInjectionService.namedService4.name == 'qualified'
+        beanInjectionService.namedService4 instanceof QualifiedNamedService
+    }
+
+    void "collection injection includes all implementations"() {
+        expect:
+        beanInjectionService.namedServices != null
+        beanInjectionService.namedServices.size() == 4
+
+        and: "all implementations are present"
+        beanInjectionService.namedServices.any { it.name == 'primary' }
+        beanInjectionService.namedServices.any { it.name == 'regular' }
+        beanInjectionService.namedServices.any { it.name == 'special' }
+        beanInjectionService.namedServices.any { it.name == 'qualified' }
+    }
+
+    void "each bean implementation returns unique name"() {
+        when:
+        def names = beanInjectionService.namedServices*.name.unique()
+
+        then:
+        names.size() == 4
+        names.containsAll(['primary', 'regular', 'special', 'qualified'])
+    }
+
+    void "beans can be distinguished by their class type"() {
+        when:
+        def types = beanInjectionService.namedServices*.getClass()*.simpleName
+
+        then:
+        types.contains('PrimaryNamedService')
+        types.contains('RegularNamedService')
+        types.contains('SpecialNamedService')
+        types.contains('QualifiedNamedService')
+    }
+}

Reply via email to