jdaugherty commented on PR #16272:
URL: https://github.com/apache/grails-core/pull/16272#issuecomment-5878126473

   **Suggested functional tests: link resolution in the `namespaces` app**
   
   Could you add these to this PR? `LinkResolutionSpec` renders links from 
pages in the default, `admin`, `manage` and `archive` namespaces, issues 
redirects and form saves with redirects turned off, and then requests each 
target to check it serves the linked record. It covers:
   
   - a `RestfulController` not named after its domain class 
(`MoviesController<Film>`): links, the redirect after a form save, and the JSON 
`Location`
   - two controllers serving one domain class in different namespaces 
(`AssessmentController`, `manage/ManageAssessmentController`), each link shape 
(`action`, `method`, a class, `g:link`, `g:form`, an explicit `controller`)
   - a controller name defined in the default namespace and in `admin` 
(`AuthorController`), a name no controller has, and a redirect from `admin` to 
a controller defined only in the default namespace
   - a report built on a generic base (`admin/GadgetReportController extends 
ReportBase<Gadget>`)
   - two domain classes with the same simple name, each with its own 
`ItemController`
   - a link to an association (`screening.film`)
   - a link naming no HTTP method, rendered by a GET, a POST and a GET again, 
and an any-method mapping (`/home-list/$page`) next to a GET-only mapping for 
the same action that needs other parameters
   
   At `5c4195c5a9` the app's 61 tests pass except 5 rows. 27 of the spec's 55 
fail on 8.0.x.
   
   The 5 that fail on this head:
   
   - `catalog.Item` and `archive.Item` each have an `ItemController` declaring 
them, and a link to one reaches the other's controller (3 rows). `/item/show/1` 
then renders the catalog item that shares the id. Dropping a by-name candidate 
that declares a different domain class makes all 3 pass in a local run.
   - `admin/GadgetReportController` takes the `Gadget` link and `redirect 
gadget` from `admin/PageController` (2 rows). These rows expect 
`GadgetController`, and the row for the report's own page expects it to keep 
linking to its own `show`; adjust them if this is settled differently.
   
   Apply from the repository root with `git apply`; it applies cleanly to 
`5c4195c5a9`.
   
   <details><summary><code>namespaces</code> patch (25 files)</summary>
   
   ```diff
   diff --git a/grails-test-examples/namespaces/build.gradle 
b/grails-test-examples/namespaces/build.gradle
   index 9e34eb7479..f3b0f73e24 100644
   --- a/grails-test-examples/namespaces/build.gradle
   +++ b/grails-test-examples/namespaces/build.gradle
   @@ -61,6 +61,7 @@ dependencies {
    
    
        integrationTestImplementation 
testFixtures('org.apache.grails:grails-geb')
   +    integrationTestImplementation 
'org.apache.grails:grails-testing-support-http-client'
    }
    
    apply {
   diff --git 
a/grails-test-examples/namespaces/grails-app/controllers/UrlMappings.groovy 
b/grails-test-examples/namespaces/grails-app/controllers/UrlMappings.groovy
   index 7b385ab86f..27215ec2b8 100644
   --- 
a/grails-test-examples/namespaces/grails-app/controllers/UrlMappings.groovy
   +++ 
b/grails-test-examples/namespaces/grails-app/controllers/UrlMappings.groovy
   @@ -37,6 +37,18 @@ class UrlMappings {
                namespace = "frontend"
            }
    
   +        "/manage/$controller/$action?/$id?(.$format)?"{
   +            namespace = "manage"
   +        }
   +
   +        "/archive/$controller/$action?/$id?(.$format)?"{
   +            namespace = "archive"
   +        }
   +
   +        "/home-submit"(controller: "home", action: "target", method: "POST")
   +        "/home-list/$page"(controller: "home", action: "list")
   +        "/home-by/$category"(controller: "home", action: "list", method: 
"GET")
   +
            "/"(view:"/index")
            "500"(view:'/error')
        }
   diff --git 
a/grails-test-examples/namespaces/grails-app/controllers/namespaces/AssessmentController.groovy
 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/AssessmentController.groovy
   new file mode 100644
   index 0000000000..ac056ffb96
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/AssessmentController.groovy
   @@ -0,0 +1,35 @@
   +/*
   + *  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 namespaces
   +
   +import grails.rest.RestfulController
   +
   +class AssessmentController extends RestfulController<Assessment> {
   +
   +    LinkFixtureService linkFixtureService
   +
   +    AssessmentController() {
   +        super(Assessment)
   +    }
   +
   +    def links() {
   +        render view: '/links/resourceLinks', model: 
linkFixtureService.model(params)
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/controllers/namespaces/AuthorController.groovy
 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/AuthorController.groovy
   new file mode 100644
   index 0000000000..b5be646af9
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/AuthorController.groovy
   @@ -0,0 +1,27 @@
   +/*
   + *  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 namespaces
   +
   +class AuthorController {
   +
   +    def index() {
   +        render 'Root Author'
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/controllers/namespaces/GadgetController.groovy
 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/GadgetController.groovy
   new file mode 100644
   index 0000000000..21eee9ac2b
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/GadgetController.groovy
   @@ -0,0 +1,29 @@
   +/*
   + *  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 namespaces
   +
   +import grails.rest.RestfulController
   +
   +class GadgetController extends RestfulController<Gadget> {
   +
   +    GadgetController() {
   +        super(Gadget)
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/controllers/namespaces/HomeController.groovy
 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/HomeController.groovy
   new file mode 100644
   index 0000000000..f1ed80c995
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/HomeController.groovy
   @@ -0,0 +1,45 @@
   +/*
   + *  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 namespaces
   +
   +class HomeController {
   +
   +    LinkFixtureService linkFixtureService
   +
   +    def index() {
   +        render 'Home'
   +    }
   +
   +    def links() {
   +        render view: '/links/resourceLinks', model: 
linkFixtureService.model(params)
   +    }
   +
   +    def methodLinks() {
   +        render view: '/links/methodLinks'
   +    }
   +
   +    def target() {
   +        render 'Target'
   +    }
   +
   +    def list() {
   +        render "List ${params.page ?: ''}${params.category ?: ''}"
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/controllers/namespaces/MoviesController.groovy
 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/MoviesController.groovy
   new file mode 100644
   index 0000000000..ee45ca6f9c
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/MoviesController.groovy
   @@ -0,0 +1,29 @@
   +/*
   + *  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 namespaces
   +
   +import grails.rest.RestfulController
   +
   +class MoviesController extends RestfulController<Film> {
   +
   +    MoviesController() {
   +        super(Film)
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/controllers/namespaces/admin/AuthorController.groovy
 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/admin/AuthorController.groovy
   new file mode 100644
   index 0000000000..c4cdad8bb2
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/admin/AuthorController.groovy
   @@ -0,0 +1,29 @@
   +/*
   + *  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 namespaces.admin
   +
   +class AuthorController {
   +
   +    static namespace = 'admin'
   +
   +    def index() {
   +        render 'Admin Author'
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/controllers/namespaces/admin/GadgetReportController.groovy
 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/admin/GadgetReportController.groovy
   new file mode 100644
   index 0000000000..9d4202c285
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/admin/GadgetReportController.groovy
   @@ -0,0 +1,39 @@
   +/*
   + *  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 namespaces.admin
   +
   +import namespaces.Gadget
   +import namespaces.LinkFixtureService
   +import namespaces.ReportBase
   +
   +class GadgetReportController extends ReportBase<Gadget> {
   +
   +    static namespace = 'admin'
   +
   +    LinkFixtureService linkFixtureService
   +
   +    def show(Long id) {
   +        render "Gadget Report ${id}"
   +    }
   +
   +    def links() {
   +        render view: '/links/resourceLinks', model: 
linkFixtureService.model(params)
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/controllers/namespaces/admin/PageController.groovy
 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/admin/PageController.groovy
   index c455fbccdd..2544b879e2 100644
   --- 
a/grails-test-examples/namespaces/grails-app/controllers/namespaces/admin/PageController.groovy
   +++ 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/admin/PageController.groovy
   @@ -19,10 +19,15 @@
    
    package namespaces.admin
    
   +import namespaces.Gadget
   +import namespaces.LinkFixtureService
   +
    class PageController {
    
        static namespace = "admin"
    
   +    LinkFixtureService linkFixtureService
   +
        def index() {
            render view: "/page/index", model: [pageTitle: "Admin Page"]
        }
   @@ -46,4 +51,20 @@ class PageController {
        def redirectToRootReport() {
            redirect controller: "report", action: "index", namespace: null
        }
   +
   +    def resourceLinks() {
   +        render view: "/links/resourceLinks", model: 
linkFixtureService.model(params)
   +    }
   +
   +    def redirectToHome() {
   +        redirect controller: "home", action: "index"
   +    }
   +
   +    def redirectToAuthor() {
   +        redirect controller: "author", action: "index"
   +    }
   +
   +    def redirectToGadget(Long id) {
   +        redirect Gadget.get(id)
   +    }
    }
   diff --git 
a/grails-test-examples/namespaces/grails-app/controllers/namespaces/archive/ItemController.groovy
 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/archive/ItemController.groovy
   new file mode 100644
   index 0000000000..2fb82a0342
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/archive/ItemController.groovy
   @@ -0,0 +1,38 @@
   +/*
   + *  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 namespaces.archive
   +
   +import grails.rest.RestfulController
   +import namespaces.LinkFixtureService
   +
   +class ItemController extends RestfulController<Item> {
   +
   +    static namespace = 'archive'
   +
   +    LinkFixtureService linkFixtureService
   +
   +    ItemController() {
   +        super(Item)
   +    }
   +
   +    def links() {
   +        render view: '/links/resourceLinks', model: 
linkFixtureService.model(params)
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/controllers/namespaces/catalog/ItemController.groovy
 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/catalog/ItemController.groovy
   new file mode 100644
   index 0000000000..b5027cad23
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/catalog/ItemController.groovy
   @@ -0,0 +1,36 @@
   +/*
   + *  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 namespaces.catalog
   +
   +import grails.rest.RestfulController
   +import namespaces.LinkFixtureService
   +
   +class ItemController extends RestfulController<Item> {
   +
   +    LinkFixtureService linkFixtureService
   +
   +    ItemController() {
   +        super(Item)
   +    }
   +
   +    def links() {
   +        render view: '/links/resourceLinks', model: 
linkFixtureService.model(params)
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/controllers/namespaces/manage/DashboardController.groovy
 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/manage/DashboardController.groovy
   new file mode 100644
   index 0000000000..d23fcb66a9
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/manage/DashboardController.groovy
   @@ -0,0 +1,38 @@
   +/*
   + *  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 namespaces.manage
   +
   +import namespaces.Film
   +import namespaces.LinkFixtureService
   +
   +class DashboardController {
   +
   +    static namespace = 'manage'
   +
   +    LinkFixtureService linkFixtureService
   +
   +    def links() {
   +        render view: '/links/resourceLinks', model: 
linkFixtureService.model(params)
   +    }
   +
   +    def redirectToFilm(Long id) {
   +        redirect Film.get(id)
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/controllers/namespaces/manage/ManageAssessmentController.groovy
 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/manage/ManageAssessmentController.groovy
   new file mode 100644
   index 0000000000..ce1d70862b
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/controllers/namespaces/manage/ManageAssessmentController.groovy
   @@ -0,0 +1,39 @@
   +/*
   + *  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 namespaces.manage
   +
   +import grails.rest.RestfulController
   +import namespaces.Assessment
   +import namespaces.LinkFixtureService
   +
   +class ManageAssessmentController extends RestfulController<Assessment> {
   +
   +    static namespace = 'manage'
   +
   +    LinkFixtureService linkFixtureService
   +
   +    ManageAssessmentController() {
   +        super(Assessment)
   +    }
   +
   +    def links() {
   +        render view: '/links/resourceLinks', model: 
linkFixtureService.model(params)
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/domain/namespaces/Assessment.groovy
 
b/grails-test-examples/namespaces/grails-app/domain/namespaces/Assessment.groovy
   new file mode 100644
   index 0000000000..2b055aeacf
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/domain/namespaces/Assessment.groovy
   @@ -0,0 +1,25 @@
   +/*
   + *  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 namespaces
   +
   +class Assessment {
   +
   +    String title
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/domain/namespaces/Film.groovy 
b/grails-test-examples/namespaces/grails-app/domain/namespaces/Film.groovy
   new file mode 100644
   index 0000000000..c68647b221
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/domain/namespaces/Film.groovy
   @@ -0,0 +1,25 @@
   +/*
   + *  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 namespaces
   +
   +class Film {
   +
   +    String title
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/domain/namespaces/Gadget.groovy 
b/grails-test-examples/namespaces/grails-app/domain/namespaces/Gadget.groovy
   new file mode 100644
   index 0000000000..a757dec5dc
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/domain/namespaces/Gadget.groovy
   @@ -0,0 +1,25 @@
   +/*
   + *  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 namespaces
   +
   +class Gadget {
   +
   +    String name
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/domain/namespaces/Screening.groovy 
b/grails-test-examples/namespaces/grails-app/domain/namespaces/Screening.groovy
   new file mode 100644
   index 0000000000..aa3ac05f2e
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/domain/namespaces/Screening.groovy
   @@ -0,0 +1,24 @@
   +/*
   + *  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 namespaces
   +
   +class Screening {
   +
   +    Film film
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/domain/namespaces/archive/Item.groovy
 
b/grails-test-examples/namespaces/grails-app/domain/namespaces/archive/Item.groovy
   new file mode 100644
   index 0000000000..7d62ce53f6
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/domain/namespaces/archive/Item.groovy
   @@ -0,0 +1,33 @@
   +/*
   + *  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 namespaces.archive
   +
   +/**
   + * Shares its simple name, and so its decapitalized name, with {@link 
namespaces.catalog.Item}.
   + */
   +class Item {
   +
   +    String name
   +
   +    static mapping = {
   +        autoImport false
   +        table 'archive_item'
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/domain/namespaces/catalog/Item.groovy
 
b/grails-test-examples/namespaces/grails-app/domain/namespaces/catalog/Item.groovy
   new file mode 100644
   index 0000000000..70de2f00b2
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/domain/namespaces/catalog/Item.groovy
   @@ -0,0 +1,25 @@
   +/*
   + *  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 namespaces.catalog
   +
   +class Item {
   +
   +    String name
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/services/namespaces/LinkFixtureService.groovy
 
b/grails-test-examples/namespaces/grails-app/services/namespaces/LinkFixtureService.groovy
   new file mode 100644
   index 0000000000..ea26eb9a7f
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/services/namespaces/LinkFixtureService.groovy
   @@ -0,0 +1,37 @@
   +/*
   + *  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 namespaces
   +
   +import grails.gorm.transactions.ReadOnly
   +
   +class LinkFixtureService {
   +
   +    @ReadOnly
   +    Map model(Map params) {
   +        [
   +                assessment : Assessment.get(params.assessmentId as Long),
   +                film       : Film.get(params.filmId as Long),
   +                gadget     : Gadget.get(params.gadgetId as Long),
   +                catalogItem: 
namespaces.catalog.Item.get(params.catalogItemId as Long),
   +                archiveItem: 
namespaces.archive.Item.get(params.archiveItemId as Long),
   +                screening  : Screening.get(params.screeningId as Long)
   +        ]
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/grails-app/views/links/methodLinks.gsp 
b/grails-test-examples/namespaces/grails-app/views/links/methodLinks.gsp
   new file mode 100644
   index 0000000000..0748857336
   --- /dev/null
   +++ b/grails-test-examples/namespaces/grails-app/views/links/methodLinks.gsp
   @@ -0,0 +1,28 @@
   +<%--
   +  ~  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.
   +  --%>
   +<!doctype html>
   +<html>
   +<head>
   +    <title>Method Links</title>
   +</head>
   +<body>
   +<a id="targetLink" href="${createLink(controller: 'home', action: 
'target')}">Target</a>
   +<a id="pagedListLink" href="${createLink(controller: 'home', action: 
'list', params: [page: 2])}">Page 2</a>
   +</body>
   +</html>
   diff --git 
a/grails-test-examples/namespaces/grails-app/views/links/resourceLinks.gsp 
b/grails-test-examples/namespaces/grails-app/views/links/resourceLinks.gsp
   new file mode 100644
   index 0000000000..d68e2455f4
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/grails-app/views/links/resourceLinks.gsp
   @@ -0,0 +1,44 @@
   +<%--
   +  ~  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.
   +  --%>
   +<%@ page import="namespaces.Assessment" %>
   +<!doctype html>
   +<html>
   +<head>
   +    <title>Resource Links</title>
   +</head>
   +<body>
   +<a id="assessmentShowLink" href="${createLink(resource: assessment, action: 
'show')}">Assessment</a>
   +<a id="assessmentEditLink" href="${createLink(resource: assessment, action: 
'edit')}">Edit Assessment</a>
   +<a id="assessmentNoActionLink" href="${createLink(resource: 
assessment)}">Assessment, no action</a>
   +<a id="assessmentGetLink" href="${createLink(resource: assessment, method: 
'GET')}">Assessment, GET</a>
   +<a id="assessmentExplicitLink" href="${createLink(resource: assessment, 
action: 'show', controller: 'assessment')}">Assessment, explicit controller</a>
   +<a id="assessmentClassLink" href="${createLink(resource: Assessment, 
action: 'show', id: assessment.id)}">Assessment, by class</a>
   +<a id="assessmentIndexLink" href="${createLink(resource: Assessment, 
action: 'index')}">Assessments</a>
   +<g:link elementId="assessmentTagLink" resource="${assessment}" 
action="show">Assessment, tag</g:link>
   +<g:form name="assessmentForm" resource="${assessment}" action="update" 
method="PUT"></g:form>
   +<a id="filmShowLink" href="${createLink(resource: film, action: 
'show')}">Film</a>
   +<a id="gadgetShowLink" href="${createLink(resource: gadget, action: 
'show')}">Gadget</a>
   +<a id="catalogItemShowLink" href="${createLink(resource: catalogItem, 
action: 'show')}">Catalog item</a>
   +<a id="archiveItemShowLink" href="${createLink(resource: archiveItem, 
action: 'show')}">Archive item</a>
   +<a id="screeningFilmLink" href="${createLink(resource: screening?.film, 
action: 'show')}">Screening film</a>
   +<a id="authorLink" href="${createLink(controller: 'author', action: 
'index')}">Author</a>
   +<a id="homeLink" href="${createLink(controller: 'home', action: 
'index')}">Home</a>
   +<a id="unknownLink" href="${createLink(controller: 'nowhere', action: 
'list')}">Unknown</a>
   +</body>
   +</html>
   diff --git 
a/grails-test-examples/namespaces/src/integration-test/groovy/namespaces/LinkResolutionSpec.groovy
 
b/grails-test-examples/namespaces/src/integration-test/groovy/namespaces/LinkResolutionSpec.groovy
   new file mode 100644
   index 0000000000..99e6f751b1
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/src/integration-test/groovy/namespaces/LinkResolutionSpec.groovy
   @@ -0,0 +1,216 @@
   +/*
   + *  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 namespaces
   +
   +import java.net.http.HttpClient
   +import java.util.regex.Matcher
   +import java.util.regex.Pattern
   +
   +import spock.lang.Shared
   +import spock.lang.Specification
   +import spock.lang.Tag
   +import spock.lang.Unroll
   +
   +import grails.testing.mixin.integration.Integration
   +import org.apache.grails.testing.http.client.HttpClientSupport
   +import org.apache.grails.testing.http.client.TestHttpResponse
   +
   +/**
   + * Pins where links and redirects built from a controller name or a domain 
instance land in a running
   + * application, across namespaces, controllers not named after their domain 
class, and controllers
   + * sharing a name.
   + */
   +@Integration(applicationClass = Application)
   +@Tag('http-client')
   +class LinkResolutionSpec extends Specification implements HttpClientSupport 
{
   +
   +    @Shared
   +    Map<String, Long> ids = [:]
   +
   +    void setup() {
   +        if (!ids) {
   +            Assessment.withNewTransaction {
   +                ids.assessment = new Assessment(title: 
'Quarterly').save(flush: true, failOnError: true).id
   +                Film film = new Film(title: 'Jaws').save(flush: true, 
failOnError: true)
   +                ids.film = film.id
   +                ids.screening = new Screening(film: film).save(flush: true, 
failOnError: true).id
   +                ids.gadget = new Gadget(name: 'Widget').save(flush: true, 
failOnError: true).id
   +                ids.catalogItem = new namespaces.catalog.Item(name: 
'Catalog').save(flush: true, failOnError: true).id
   +                ids.archiveItem = new namespaces.archive.Item(name: 
'Archive').save(flush: true, failOnError: true).id
   +            }
   +        }
   +    }
   +
   +    @Unroll
   +    void "#element rendered by #page targets #expected"(String page, String 
element, String expected, String lands) {
   +        when: 'the page is rendered'
   +        TestHttpResponse response = http("${page}?${fixtureQuery()}")
   +
   +        then:
   +        response.statusCode() == 200
   +
   +        when:
   +        String target = linkTarget(response.body() as String, element)
   +
   +        then: 'the link targets the expected controller'
   +        target == contextPath + fill(expected)
   +
   +        and: 'the target serves the linked resource'
   +        lands == null || bodyAt(target).contains(lands)
   +
   +        where:
   +        page                             | element                  || 
expected                                       | lands
   +        '/home/links'                    | 'assessmentShowLink'     || 
'/assessment/show/{assessment}'                | 'Quarterly'
   +        '/home/links'                    | 'assessmentEditLink'     || 
'/assessment/edit/{assessment}'                | null
   +        '/home/links'                    | 'assessmentNoActionLink' || 
'/assessment/index/{assessment}'               | null
   +        '/home/links'                    | 'assessmentGetLink'      || 
'/assessment/show/{assessment}'                | 'Quarterly'
   +        '/home/links'                    | 'assessmentClassLink'    || 
'/assessment/show/{assessment}'                | 'Quarterly'
   +        '/home/links'                    | 'assessmentIndexLink'    || 
'/assessment/index'                            | null
   +        '/home/links'                    | 'assessmentTagLink'      || 
'/assessment/show/{assessment}'                | 'Quarterly'
   +        '/home/links'                    | 'assessmentForm'         || 
'/assessment/update/{assessment}'              | null
   +        '/home/links'                    | 'filmShowLink'           || 
'/movies/show/{film}'                          | 'Jaws'
   +        '/home/links'                    | 'gadgetShowLink'         || 
'/gadget/show/{gadget}'                        | 'Widget'
   +        '/home/links'                    | 'catalogItemShowLink'    || 
'/item/show/{catalogItem}'                     | 'Catalog'
   +        '/home/links'                    | 'archiveItemShowLink'    || 
'/archive/item/show/{archiveItem}'             | 'Archive'
   +        '/home/links'                    | 'screeningFilmLink'      || 
'/movies/show/{film}'                          | 'Jaws'
   +        '/home/links'                    | 'authorLink'             || 
'/author/index'                                | 'Root Author'
   +        '/home/links'                    | 'unknownLink'            || 
'/nowhere/list'                                | null
   +        '/assessment/links'              | 'assessmentShowLink'     || 
'/assessment/show/{assessment}'                | 'Quarterly'
   +        '/assessment/links'              | 'assessmentNoActionLink' || 
'/assessment/index/{assessment}'                 | null
   +        '/assessment/links'              | 'assessmentIndexLink'    || 
'/assessment/index'                            | null
   +        '/manage/manageAssessment/links' | 'assessmentShowLink'     || 
'/manage/manageAssessment/show/{assessment}'   | 'Quarterly'
   +        '/manage/manageAssessment/links' | 'assessmentEditLink'     || 
'/manage/manageAssessment/edit/{assessment}'   | null
   +        '/manage/manageAssessment/links' | 'assessmentNoActionLink' || 
'/manage/manageAssessment/index/{assessment}'  | null
   +        '/manage/manageAssessment/links' | 'assessmentGetLink'      || 
'/manage/manageAssessment/show/{assessment}'   | 'Quarterly'
   +        '/manage/manageAssessment/links' | 'assessmentClassLink'    || 
'/manage/manageAssessment/show/{assessment}'   | 'Quarterly'
   +        '/manage/manageAssessment/links' | 'assessmentIndexLink'    || 
'/manage/manageAssessment/index'               | null
   +        '/manage/manageAssessment/links' | 'assessmentTagLink'      || 
'/manage/manageAssessment/show/{assessment}'   | 'Quarterly'
   +        '/manage/manageAssessment/links' | 'assessmentForm'         || 
'/manage/manageAssessment/update/{assessment}' | null
   +        '/manage/manageAssessment/links' | 'assessmentExplicitLink' || 
'/assessment/show/{assessment}'                | 'Quarterly'
   +        '/manage/dashboard/links'        | 'assessmentShowLink'     || 
'/manage/manageAssessment/show/{assessment}'   | 'Quarterly'
   +        '/manage/dashboard/links'        | 'filmShowLink'           || 
'/movies/show/{film}'                          | 'Jaws'
   +        '/manage/dashboard/links'        | 'authorLink'             || 
'/author/index'                                | 'Root Author'
   +        '/manage/dashboard/links'        | 'unknownLink'            || 
'/manage/nowhere/list'                         | null
   +        '/admin/page/resourceLinks'      | 'assessmentShowLink'     || 
'/assessment/show/{assessment}'                | 'Quarterly'
   +        '/admin/page/resourceLinks'      | 'filmShowLink'           || 
'/movies/show/{film}'                          | 'Jaws'
   +        '/admin/page/resourceLinks'      | 'gadgetShowLink'         || 
'/gadget/show/{gadget}'                        | 'Widget'
   +        '/admin/page/resourceLinks'      | 'authorLink'             || 
'/admin/author/index'                          | 'Admin Author'
   +        '/admin/page/resourceLinks'      | 'homeLink'               || 
'/home/index'                                  | 'Home'
   +        '/admin/page/resourceLinks'      | 'unknownLink'            || 
'/admin/nowhere/list'                          | null
   +        '/admin/gadgetReport/links'      | 'gadgetShowLink'         || 
'/admin/gadgetReport/show/{gadget}'            | 'Gadget Report'
   +        '/archive/item/links'            | 'archiveItemShowLink'    || 
'/archive/item/show/{archiveItem}'             | 'Archive'
   +        '/archive/item/links'            | 'catalogItemShowLink'    || 
'/item/show/{catalogItem}'                     | 'Catalog'
   +        '/item/links'                    | 'catalogItemShowLink'    || 
'/item/show/{catalogItem}'                     | 'Catalog'
   +        '/item/links'                    | 'archiveItemShowLink'    || 
'/archive/item/show/{archiveItem}'             | 'Archive'
   +    }
   +
   +    @Unroll
   +    void "#description redirects to #expected"(String description, String 
path, String form, String expected, String lands) {
   +        given: 'a client that reports redirects rather than following them'
   +        HttpClient client = newHttpClientWith { 
followRedirects(HttpClient.Redirect.NEVER) }
   +
   +        when:
   +        TestHttpResponse response = form != null ?
   +                httpPost([:], fill(path), form, 
'application/x-www-form-urlencoded', client) :
   +                http(fill(path), client)
   +
   +        then:
   +        response.statusCode() == 302
   +
   +        when:
   +        String target = new URI(response.headerValue('Location')).path
   +
   +        then: 'the redirect targets the expected controller'
   +        target ==~ Pattern.quote(contextPath) + 
fill(expected).split(/\{new\}/, -1).collect { Pattern.quote(it) }.join(/\d+/)
   +
   +        and: 'the target serves the resource'
   +        lands == null || bodyAt(target).contains(lands)
   +
   +        where:
   +        description                                               | path    
                                  | form           || expected                  
                 | lands
   +        'a form save on a renamed RestfulController'              | 
'/movies/save'                            | 'title=Alien'  || 
'/movies/show/{new}'                        | 'Alien'
   +        'a form save on a namespaced RestfulController'           | 
'/manage/manageAssessment/save'           | 'title=Annual' || 
'/manage/manageAssessment/show/{new}'       | 'Annual'
   +        'a form save on the controller named after the domain'    | 
'/assessment/save'                        | 'title=Weekly' || 
'/assessment/show/{new}'                    | 'Weekly'
   +        'a form save on a namespaced same-named controller'       | 
'/archive/item/save'                      | 'name=Old'     || 
'/archive/item/show/{new}'                  | 'Old'
   +        'a form save on the default-namespace same-named one'     | 
'/item/save'                              | 'name=New'     || 
'/item/show/{new}'                          | 'New'
   +        'an admin redirect to a controller only in the default'   | 
'/admin/page/redirectToHome'              | null           || '/home/index'     
                          | 'Home'
   +        'an admin redirect to a name in the default and admin'    | 
'/admin/page/redirectToAuthor'            | null           || 
'/admin/author/index'                       | 'Admin Author'
   +        'an admin redirect to a controller only in admin'         | 
'/admin/page/redirectToBook'              | null           || 
'/admin/book/index'                         | null
   +        'a manage redirect to a film'                             | 
'/manage/dashboard/redirectToFilm/{film}' | null           || 
'/movies/show/{film}'                       | 'Jaws'
   +        'an admin redirect to a gadget'                           | 
'/admin/page/redirectToGadget/{gadget}'   | null           || 
'/gadget/show/{gadget}'                     | 'Widget'
   +    }
   +
   +    void "a JSON save on a renamed RestfulController sets a Location naming 
it"() {
   +        when:
   +        TestHttpResponse response = httpPostJson([Accept: 
'application/json'], '/movies/save', [title: 'Heat'])
   +
   +        then:
   +        response.statusCode() == 201
   +        new URI(response.headerValue('Location')).path ==~ 
Pattern.quote("${contextPath}/movies/show/") + /\d+/
   +    }
   +
   +    void "a link naming no HTTP method is resolved for each request's 
method, even once cached"() {
   +        when: 'the same page is rendered by a GET, a POST and a GET again'
   +        String viaGet = linkTarget(http('/home/methodLinks').body() as 
String, 'targetLink')
   +        String viaPost = linkTarget(httpPost([:], '/home/methodLinks', '', 
'application/x-www-form-urlencoded').body() as String, 'targetLink')
   +        String viaGetAgain = linkTarget(http('/home/methodLinks').body() as 
String, 'targetLink')
   +
   +        then: 'the POST uses the mapping for POST, and neither is served 
the other from the cache'
   +        viaGet == "${contextPath}/home/target"
   +        viaPost == "${contextPath}/home-submit"
   +        viaGetAgain == viaGet
   +    }
   +
   +    void "a link fitting an any-method mapping is not sent to the wildcard 
by a GET mapping needing other parameters"() {
   +        when:
   +        String target = linkTarget(http('/home/methodLinks').body() as 
String, 'pagedListLink')
   +
   +        then:
   +        target == "${contextPath}/home-list/2"
   +        bodyAt(target).contains('List 2')
   +    }
   +
   +    private String fixtureQuery() {
   +        ids.collect { key, value -> "${key}Id=${value}" }.join('&')
   +    }
   +
   +    private String fill(String template) {
   +        template.replaceAll(/\{(\w+)\}/) { List<String> match ->
   +            match[1] == 'new' ? match[0] : String.valueOf(ids[match[1]])
   +        }
   +    }
   +
   +    private String getContextPath() {
   +        new URI(httpBaseUrl).path
   +    }
   +
   +    private String bodyAt(String path) {
   +        URI base = new URI(httpBaseUrl)
   +        TestHttpResponse response = http([Accept: 'application/json'], 
"${base.scheme}://${base.authority}${path}")
   +        response.statusCode() == 200 ? response.body() as String : "HTTP 
${response.statusCode()}"
   +    }
   +
   +    private static String linkTarget(String html, String element) {
   +        Matcher tag = html =~ 
/<(?:a|form)\b[^>]*\bid="${Pattern.quote(element)}"[^>]*>/
   +        assert tag.find(), "no element with id ${element}"
   +        Matcher attribute = tag.group() =~ /\b(?:href|action)="([^"]*)"/
   +        assert attribute.find(), "no target on ${tag.group()}"
   +        attribute.group(1).replace('&amp;', '&')
   +    }
   +}
   diff --git 
a/grails-test-examples/namespaces/src/main/groovy/namespaces/ReportBase.groovy 
b/grails-test-examples/namespaces/src/main/groovy/namespaces/ReportBase.groovy
   new file mode 100644
   index 0000000000..002a7ade17
   --- /dev/null
   +++ 
b/grails-test-examples/namespaces/src/main/groovy/namespaces/ReportBase.groovy
   @@ -0,0 +1,27 @@
   +/*
   + *  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 namespaces
   +
   +/**
   + * A generic base a report controller extends for the domain class it 
reports on, as an application
   + * might for reports that are not the controller serving that domain class.
   + */
   +abstract class ReportBase<T> {
   +}
   ```
   
   </details>


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to