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 <script> 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('<') || + $('#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') + } +}
