You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by jd...@apache.org on 2026/10/16 15:01:46 UTC
(grails-core) branch test/rest-renderers-7.0.x updated: Tighten the Atom and HAL XML renderer specs
This is an automated email from the ASF dual-hosted git repository.
jdaugherty pushed a commit to branch test/rest-renderers-7.0.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git
The following commit(s) were added to refs/heads/test/rest-renderers-7.0.x by this push:
new 49090ba585 Tighten the Atom and HAL XML renderer specs
49090ba585 is described below
commit 49090ba5859e425296dbcbb802bef6b95d9742bc
Author: James Daugherty <jd...@jdresources.net>
AuthorDate: Fri Oct 16 11:01:23 2026 -0400
Tighten the Atom and HAL XML renderer specs
Give the Atom spec its own entity and POGO fixtures instead of importing the
HAL JSON spec's Product and SimpleProduct across packages, so the Atom
coverage no longer depends on another spec's fixture file.
Cover the public Atom tag id generation for plain paths, explicit ids,
creation dates, fragments and host-only URIs.
Cover the HAL XML link attributes written for an object's own links
(templated and deprecated), relative links, pretty printing and the
custom mime-type constructor.
---
.../web/rest/render/atom/AtomRendererSpec.groovy | 36 +++++++++++----
.../web/rest/render/hal/HalXmlRendererSpec.groovy | 54 ++++++++++++++++++++++
2 files changed, 82 insertions(+), 8 deletions(-)
diff --git a/grails-rest-transforms/src/test/groovy/org/grails/plugins/web/rest/render/atom/AtomRendererSpec.groovy b/grails-rest-transforms/src/test/groovy/org/grails/plugins/web/rest/render/atom/AtomRendererSpec.groovy
index a59d6c5522..e52c2b542b 100644
--- a/grails-rest-transforms/src/test/groovy/org/grails/plugins/web/rest/render/atom/AtomRendererSpec.groovy
+++ b/grails-rest-transforms/src/test/groovy/org/grails/plugins/web/rest/render/atom/AtomRendererSpec.groovy
@@ -33,8 +33,6 @@ import org.grails.core.lifecycle.ShutdownOperations
import org.grails.datastore.mapping.keyvalue.mapping.config.KeyValueMappingContext
import org.grails.plugins.web.mime.MimeTypesConfiguration
import org.grails.plugins.web.rest.render.ServletRenderContext
-import org.grails.plugins.web.rest.render.hal.Product
-import org.grails.plugins.web.rest.render.hal.SimpleProduct
import org.grails.support.MockApplicationContext
import org.grails.web.converters.configuration.ConvertersConfigurationHolder
import org.grails.web.converters.configuration.ConvertersConfigurationInitializer
@@ -150,12 +148,12 @@ class AtomRendererSpec extends Specification {
void 'Test Atom rejects non domain objects for #collection'() {
given:
- def renderer = configureRenderer(collection ? new AtomCollectionRenderer(SimpleProduct) : new AtomRenderer(SimpleProduct))
+ def renderer = configureRenderer(collection ? new AtomCollectionRenderer(AtomNote) : new AtomRenderer(AtomNote))
def webRequest = boundMimeTypeRequest()
- def product = new SimpleProduct(name: 'MacBook')
+ def note = new AtomNote(title: 'Draft')
when:
- renderer.render(collection ? [product] : product, new ServletRenderContext(webRequest))
+ renderer.render(collection ? [note] : note, new ServletRenderContext(webRequest))
then:
def error = thrown(IllegalArgumentException)
@@ -167,21 +165,34 @@ class AtomRendererSpec extends Specification {
void 'Test Atom rejects domain objects without lastUpdated'() {
given:
- def renderer = configureRenderer(new AtomRenderer(Product))
+ def renderer = configureRenderer(new AtomRenderer(AtomAuthor))
def webRequest = boundMimeTypeRequest()
when:
- renderer.render(new Product(name: 'MacBook'), new ServletRenderContext(webRequest))
+ renderer.render(new AtomAuthor(name: 'Graeme'), new ServletRenderContext(webRequest))
then:
def error = thrown(IllegalArgumentException)
error.message.contains("The AtomRenderer can only be used with domain classes that specify 'dateCreated' and 'lastUpdated' properties")
}
+ void 'Test Atom tag ids derive from the resource URI with #description'() {
+ expect:
+ AtomRenderer.generateIdForURI(url, dateCreated, id) == expected
+
+ where:
+ description | url | dateCreated | id || expected
+ 'a resource path' | 'http://localhost/books' | null | null || 'tag:localhost:/books'
+ 'an explicit identifier' | 'https://localhost/books/1' | null | 1L || 'tag:localhost:1'
+ 'a creation date' | 'http://localhost/books/1' | new GregorianCalendar(2013, Calendar.NOVEMBER, 8).time | 1L || 'tag:localhost,2013-11-08:1'
+ 'a fragment' | 'http://example.org/books#1' | null | null || 'tag:example.org:/books/1'
+ 'no path' | 'localhost' | null | null || 'tag:localhost'
+ }
+
private AtomRenderer configureRenderer(AtomRenderer renderer) {
def mappingContext = new KeyValueMappingContext('')
mappingContext.addPersistentEntity(AtomBook)
- mappingContext.addPersistentEntity(Product)
+ mappingContext.addPersistentEntity(AtomAuthor)
renderer.mappingContext = mappingContext
def messages = new StaticMessageSource()
messages.addMessage('resource.atomBook.href.title', Locale.ENGLISH, 'Books')
@@ -239,3 +250,12 @@ class AtomBook {
Date dateCreated
Date lastUpdated
}
+
+@Entity
+class AtomAuthor {
+ String name
+}
+
+class AtomNote {
+ String title
+}
diff --git a/grails-rest-transforms/src/test/groovy/org/grails/plugins/web/rest/render/hal/HalXmlRendererSpec.groovy b/grails-rest-transforms/src/test/groovy/org/grails/plugins/web/rest/render/hal/HalXmlRendererSpec.groovy
index 2802647132..437a337e05 100644
--- a/grails-rest-transforms/src/test/groovy/org/grails/plugins/web/rest/render/hal/HalXmlRendererSpec.groovy
+++ b/grails-rest-transforms/src/test/groovy/org/grails/plugins/web/rest/render/hal/HalXmlRendererSpec.groovy
@@ -20,6 +20,7 @@ package org.grails.plugins.web.rest.render.hal
import grails.core.DefaultGrailsApplication
import grails.core.GrailsApplication
+import grails.rest.Link
import grails.rest.render.hal.HalXmlCollectionRenderer
import grails.rest.render.hal.HalXmlRenderer
import grails.spring.BeanBuilder
@@ -27,6 +28,7 @@ import grails.util.GrailsWebMockUtil
import grails.util.GrailsWebUtil
import grails.web.CamelCaseUrlConverter
import grails.web.mapping.LinkGenerator
+import grails.web.mime.MimeType
import org.grails.config.PropertySourcesConfig
import org.grails.core.lifecycle.ShutdownOperations
import org.grails.datastore.mapping.keyvalue.mapping.config.KeyValueMappingContext
@@ -175,6 +177,46 @@ class HalXmlRendererSpec extends Specification {
'context excludes' | null | [] | [excludes: ['numberInStock', 'category']]
}
+ void 'Test HAL XML writes the extra links declared by the rendered object'() {
+ given:
+ def renderer = configureRenderer(new HalXmlRenderer(LinkedGadget))
+ def webRequest = boundMimeTypeRequest()
+ def response = webRequest.response as MockHttpServletResponse
+
+ when:
+ renderer.render(new LinkedGadget(name: 'Gadget'), new ServletRenderContext(webRequest))
+
+ then:
+ response.contentAsString.contains('<resource href="http://localhost/products" hreflang="en" title="Products">')
+ response.contentAsString.contains('<link rel="docs" href="http://example.org/docs" hreflang="en" templated="true" deprecated="true" />')
+ response.contentAsString.contains('<name>Gadget</name>')
+ }
+
+ void 'Test HAL XML supports relative links pretty printing and a custom mime type'() {
+ given:
+ def pretty = configureRenderer(new HalXmlRenderer(Product, MimeType.XML))
+ pretty.absoluteLinks = false
+ pretty.prettyPrint = true
+ def compact = configureRenderer(new HalXmlRenderer(Product))
+ compact.absoluteLinks = false
+ def product = new Product(name: 'MacBook', numberInStock: 10)
+
+ when:
+ def prettyRequest = boundMimeTypeRequest()
+ pretty.render(product, new ServletRenderContext(prettyRequest))
+ String prettyXml = (prettyRequest.response as MockHttpServletResponse).contentAsString
+ def compactRequest = boundMimeTypeRequest()
+ compact.render(product, new ServletRenderContext(compactRequest))
+ String compactXml = (compactRequest.response as MockHttpServletResponse).contentAsString
+
+ then:
+ pretty.mimeTypes.toList() == [MimeType.XML]
+ compactXml.contains('<resource href="/products" hreflang="en" title="Products">')
+ !compactXml.contains('http://localhost')
+ prettyXml != compactXml
+ prettyXml.replaceAll(/\s*\R\s*/, '') == compactXml
+ }
+
private HalXmlRenderer configureRenderer(HalXmlRenderer renderer) {
def mappingContext = new KeyValueMappingContext('')
mappingContext.addPersistentEntity(Product)
@@ -234,3 +276,15 @@ class HalXmlRendererSpec extends Specification {
webRequest
}
}
+
+class LinkedGadget {
+
+ String name
+
+ Collection<Link> links() {
+ def docs = new Link('docs', 'http://example.org/docs')
+ docs.templated = true
+ docs.deprecated = true
+ [docs]
+ }
+}