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]
+    }
+}