diff --git a/example/grails-app/views/_requestState.gsp b/example/grails-app/views/_requestState.gsp new file mode 100644 index 0000000..7f49023 --- /dev/null +++ b/example/grails-app/views/_requestState.gsp @@ -0,0 +1,8 @@ + + +

${request.getAttribute('foo')}

+

${params.q}

+

${params.id}

+

${session.getAttribute('user')}

+ + diff --git a/example/grails-app/views/_taglib.gsp b/example/grails-app/views/_taglib.gsp new file mode 100644 index 0000000..054f4b1 --- /dev/null +++ b/example/grails-app/views/_taglib.gsp @@ -0,0 +1,5 @@ + + + + + diff --git a/example/src/integration-test/groovy/grails/plugins/rendering/document/ProductionEnvironmentSpec.groovy b/example/src/integration-test/groovy/grails/plugins/rendering/document/ProductionEnvironmentSpec.groovy new file mode 100644 index 0000000..324c832 --- /dev/null +++ b/example/src/integration-test/groovy/grails/plugins/rendering/document/ProductionEnvironmentSpec.groovy @@ -0,0 +1,29 @@ +package grails.plugins.rendering.document + +import grails.util.Environment +import org.grails.web.servlet.WrappedResponseHolder +import org.springframework.web.context.request.RequestContextHolder +import spock.lang.Specification + +abstract class ProductionEnvironmentSpec extends Specification { + + private String previousEnv + + def setup() { + previousEnv = System.getProperty(Environment.KEY) + System.setProperty(Environment.KEY, Environment.PRODUCTION.name) + assert Environment.current == Environment.PRODUCTION + RequestContextHolder.resetRequestAttributes() + WrappedResponseHolder.wrappedResponse = null + } + + def cleanup() { + if (previousEnv == null) { + System.clearProperty(Environment.KEY) + } else { + System.setProperty(Environment.KEY, previousEnv) + } + RequestContextHolder.resetRequestAttributes() + WrappedResponseHolder.wrappedResponse = null + } +} diff --git a/example/src/integration-test/groovy/grails/plugins/rendering/document/RenderEnvironmentSpec.groovy b/example/src/integration-test/groovy/grails/plugins/rendering/document/RenderEnvironmentSpec.groovy new file mode 100644 index 0000000..700ab5e --- /dev/null +++ b/example/src/integration-test/groovy/grails/plugins/rendering/document/RenderEnvironmentSpec.groovy @@ -0,0 +1,219 @@ +package grails.plugins.rendering.document + +import grails.core.GrailsApplication +import grails.testing.mixin.integration.Integration +import grails.util.GrailsWebMockUtil +import org.grails.web.servlet.WrappedResponseHolder +import org.grails.web.servlet.mvc.GrailsWebRequest +import org.springframework.context.support.StaticApplicationContext +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.web.context.request.RequestAttributes +import org.springframework.web.context.request.RequestContextHolder +import org.springframework.web.context.request.ServletRequestAttributes +import org.springframework.web.servlet.DispatcherServlet +import org.springframework.web.servlet.i18n.FixedLocaleResolver +import org.springframework.web.servlet.support.RequestContextUtils + +@Integration +class RenderEnvironmentSpec extends ProductionEnvironmentSpec { + + GrailsApplication grailsApplication + + def "bind a web request when one does not exist"() { + given: + def out = new StringWriter() + GrailsWebRequest bound = null + + when: + RenderEnvironment.with(grailsApplication.mainContext, out) { + bound = GrailsWebRequest.lookup() + } + + then: + bound != null + bound.out.is(out) + } + + def "leave no request bound afterwards"() { + when: + RenderEnvironment.with(grailsApplication.mainContext, new StringWriter()) {} + + then: + RequestContextHolder.requestAttributes == null + WrappedResponseHolder.wrappedResponse == null + } + + def "reuse the callers request and restore it afterwards"() { + given: + GrailsWebRequest original = GrailsWebMockUtil.bindMockWebRequest(grailsApplication.mainContext) + original.controllerName = 'book' + def callerOut = new StringWriter() + original.out = callerOut + def callerLocaleResolver = new FixedLocaleResolver(Locale.ITALIAN) + original.currentRequest.setAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE, callerLocaleResolver) + def layoutBufferingResponse = new MockHttpServletResponse() + WrappedResponseHolder.wrappedResponse = layoutBufferingResponse + def renderOut = new StringWriter() + GrailsWebRequest bound = null + Writer boundOut = null + String controllerName = null + + when: + RenderEnvironment.with(grailsApplication.mainContext, renderOut) { RenderEnvironment env -> + bound = GrailsWebRequest.lookup() + boundOut = bound.out + controllerName = env.controllerName + } + + then: + bound.is(original) + boundOut.is(renderOut) + controllerName == 'book' + RequestContextHolder.requestAttributes.is(original) + original.out.is(callerOut) + original.currentRequest.getAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE).is(callerLocaleResolver) + WrappedResponseHolder.wrappedResponse.is(layoutBufferingResponse) + } + + def "restore state when the block throws"() { + given: + def callerOut = new StringWriter() + GrailsWebRequest original = GrailsWebMockUtil.bindMockWebRequest(grailsApplication.mainContext) + original.out = callerOut + + when: + RenderEnvironment.with(grailsApplication.mainContext, new StringWriter()) { + throw new IllegalStateException('boom') + } + + then: + thrown(IllegalStateException) + RequestContextHolder.requestAttributes.is(original) + original.out.is(callerOut) + } + + def "restore the outer renders writer after a nested render"() { + given: + def outerOut = new StringWriter() + Writer afterInner = null + + when: + RenderEnvironment.with(grailsApplication.mainContext, outerOut) { + RenderEnvironment.with(grailsApplication.mainContext, new StringWriter()) {} + afterInner = GrailsWebRequest.lookup().out + } + + then: + afterInner.is(outerOut) + RequestContextHolder.requestAttributes == null + } + + def "render locale is #expected when explicit=#explicit and request=#requestLocale"() { + given: + if (requestLocale) { + GrailsWebRequest original = GrailsWebMockUtil.bindMockWebRequest(grailsApplication.mainContext) + (original.currentRequest as MockHttpServletRequest).addPreferredLocale(requestLocale) + } + Locale seen = null + + when: + RenderEnvironment.with(grailsApplication.mainContext, new StringWriter(), explicit) { + seen = RequestContextUtils.getLocale(GrailsWebRequest.lookup().currentRequest) + } + + then: + seen == expected + + where: + explicit | requestLocale | expected + Locale.FRENCH | Locale.GERMAN | Locale.FRENCH + null | Locale.GERMAN | Locale.GERMAN + null | null | Locale.default + } + + def "expose the application context path outside of a web request"() { + given: + String contextPath = null + + when: + RenderEnvironment.with(grailsApplication.mainContext, new StringWriter(), { + contextPath = GrailsWebRequest.lookup().contextPath + }) + + then: + contextPath == '/rendering' + } + + def "inherit the locale of a plain servlet request"() { + given: + def request = new MockHttpServletRequest() + request.addPreferredLocale(Locale.JAPANESE) + def plain = new ServletRequestAttributes(request) + RequestContextHolder.requestAttributes = plain + Locale seen = null + + when: + RenderEnvironment.with(grailsApplication.mainContext, new StringWriter(), { + seen = RequestContextUtils.getLocale(GrailsWebRequest.lookup().currentRequest) + }) + + then: + seen == Locale.JAPANESE + RequestContextHolder.requestAttributes.is(plain) + } + + def "run request destruction callbacks registered during the render"() { + given: + boolean destroyed = false + + when: + RenderEnvironment.with(grailsApplication.mainContext, new StringWriter(), { + GrailsWebRequest.lookup().registerDestructionCallback('probe', { destroyed = true } as Runnable, + RequestAttributes.SCOPE_REQUEST) + }) + + then: + destroyed + } + + def "leave the caller's request destruction callbacks alone"() { + given: + GrailsWebRequest original = GrailsWebMockUtil.bindMockWebRequest(grailsApplication.mainContext) + boolean destroyed = false + original.registerDestructionCallback('probe', { destroyed = true } as Runnable, RequestAttributes.SCOPE_REQUEST) + + when: + RenderEnvironment.with(grailsApplication.mainContext, new StringWriter()) {} + + then: + !destroyed + } + + def "restore state when a destruction callback throws"() { + when: + RenderEnvironment.with(grailsApplication.mainContext, new StringWriter()) { + GrailsWebRequest.lookup().registerDestructionCallback('probe', { throw new IllegalStateException('boom') } as Runnable, RequestAttributes.SCOPE_REQUEST) + } + + then: + thrown(IllegalStateException) + RequestContextHolder.requestAttributes == null + WrappedResponseHolder.wrappedResponse == null + } + + def "bind a web request for a non web application context"() { + given: + def out = new StringWriter() + GrailsWebRequest bound = null + + when: + RenderEnvironment.with(new StaticApplicationContext(), out) { + bound = GrailsWebRequest.lookup() + } + + then: + bound.out.is(out) + RequestContextHolder.requestAttributes == null + } +} diff --git a/example/src/integration-test/groovy/grails/plugins/rendering/document/XhtmlDocumentServiceSpec.groovy b/example/src/integration-test/groovy/grails/plugins/rendering/document/XhtmlDocumentServiceSpec.groovy index b31b8c7..3ca3e95 100644 --- a/example/src/integration-test/groovy/grails/plugins/rendering/document/XhtmlDocumentServiceSpec.groovy +++ b/example/src/integration-test/groovy/grails/plugins/rendering/document/XhtmlDocumentServiceSpec.groovy @@ -15,18 +15,90 @@ */ package grails.plugins.rendering.document +import grails.core.GrailsApplication import grails.testing.mixin.integration.Integration -import spock.lang.Specification +import grails.util.GrailsWebMockUtil +import org.grails.web.servlet.mvc.GrailsWebRequest +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.web.context.request.RequestContextHolder +import org.w3c.dom.Document -/** - * The xhtmlDocumentService is exercised in the rendering - * service tests. This is here for testing future functionality. - */ @Integration -class XhtmlDocumentServiceSpec extends Specification { +class XhtmlDocumentServiceSpec extends ProductionEnvironmentSpec { + + XhtmlDocumentService xhtmlDocumentService + GrailsApplication grailsApplication + + def "render a taglib template outside web request"() { + when: + Document document = xhtmlDocumentService.createDocument(template: '/taglib', model: [value: 'from a job']) + + then: + hiddenFieldValue(document) == 'from a job' + RequestContextHolder.requestAttributes == null + } + + def "render taglib template on a background thread"() { + given: + Document document = null + Throwable error = null + + when: + def thread = Thread.start { + try { + document = xhtmlDocumentService.createDocument(template: '/taglib', model: [value: 'from a thread']) + } catch (Throwable t) { + error = t + } + } + thread.join(30_000) + + then: + !thread.alive + error == null + hiddenFieldValue(document) == 'from a thread' + } - def xhtmlDocumentService + def "does not write into current request's output"() { + given: + GrailsWebRequest original = GrailsWebMockUtil.bindMockWebRequest(grailsApplication.mainContext) + def requestOut = new StringWriter() + original.out = requestOut + + when: + Document document = xhtmlDocumentService.createDocument(template: '/taglib', model: [value: 'in a request']) + + then: + hiddenFieldValue(document) == 'in a request' + original.out.is(requestOut) + requestOut.toString() == '' + RequestContextHolder.requestAttributes.is(original) + } + + def "renders template that reads caller's request state"() { + given: + GrailsWebRequest original = GrailsWebMockUtil.bindMockWebRequest(grailsApplication.mainContext) + MockHttpServletRequest request = original.currentRequest as MockHttpServletRequest + request.setAttribute('foo', 'bar') + request.addParameter('q', 'query') + request.session.setAttribute('user', 'alice') + original.params.id = '42' + + when: + Document document = xhtmlDocumentService.createDocument(template: '/requestState') + + then: + paragraphs(document) == ['bar', 'query', '42', 'alice'] + original.currentResponse.contentType == null + RequestContextHolder.requestAttributes.is(original) + } + + private static String hiddenFieldValue(Document document) { + document.getElementsByTagName('input').item(0).getAttribute('value') + } - def emptyScenario() { + private static List paragraphs(Document document) { + def nodes = document.getElementsByTagName('p') + (0..