You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ma...@apache.org on 2026/04/17 11:00:03 UTC

(grails-spring-security) 06/06: test: update tests for Grails 8 and SB4

This is an automated email from the ASF dual-hosted git repository.

matrei pushed a commit to branch grails8-sb4
in repository https://gitbox.apache.org/repos/asf/grails-spring-security.git

commit 983e872c008c8958a5b2a7b723c488a6ed2c450b
Author: Mattias Reichel <ma...@gmail.com>
AuthorDate: Fri Apr 17 12:57:30 2026 +0200

    test: update tests for Grails 8 and SB4
---
 .../com/testacl/ErrorsController.groovy            |   2 +-
 .../groovy/pages/AccessDeniedPage.groovy           |   8 ++
 ...owReportPage.groovy => DeleteReportPage.groovy} |  10 +-
 .../groovy/pages/EditReportPage.groovy             |  11 +-
 .../groovy/pages/ListReportPage.groovy             |  32 ++++-
 .../integration-test/groovy/pages/LoginPage.groovy |   2 +-
 .../groovy/pages/LogoutPage.groovy                 |   9 +-
 .../groovy/pages/ReportGrantPage.groovy            |  20 ++-
 .../groovy/pages/ResetDataPage.groovy              |  12 +-
 .../groovy/pages/ScaffoldPage.groovy               |   3 +-
 .../groovy/pages/ShowReportPage.groovy             |   8 +-
 .../groovy/test/AbstractSecuritySpec.groovy        |  15 +-
 .../groovy/test/AdminFunctionalSpec.groovy         | 105 +++++++-------
 .../groovy/test/User1FunctionalSpec.groovy         | 136 +++++++++---------
 .../groovy/test/User2FunctionalSpec.groovy         | 113 +++++++--------
 .../TestClassAnnotatedServiceMetadataSpec.groovy}  |  45 +++---
 .../acl/TestClassAnnotatedServiceSpec.groovy       |  21 ++-
 ...uredAnnotationSecurityMetadataSourceSpec.groovy |  58 ++++++++
 .../grails-app/views/testRequestmap/edit.gsp       |   5 +-
 .../grails-app/views/testRequestmap/show.gsp       |   4 +-
 .../grails-app/views/testRole/create.gsp           |   4 +-
 .../grails-app/views/testRole/edit.gsp             |   4 +-
 .../grails-app/views/testRole/show.gsp             |   4 +-
 .../grails-app/views/testUser/edit.gsp             |   4 +-
 .../grails-app/views/testUser/show.gsp             |   2 +-
 .../groovy/pages/CreatePage.groovy                 |   2 +-
 .../integration-test/groovy/pages/EditPage.groovy  |   4 +-
 .../groovy/pages/ScaffoldPage.groovy               |   5 +-
 .../integration-test/groovy/pages/ShowPage.groovy  |   4 +-
 .../pages/requestmap/CreateRequestmapPage.groovy   |   7 +-
 .../pages/requestmap/EditRequestmapPage.groovy     |   5 +-
 .../pages/requestmap/ListRequestmapPage.groovy     |   8 +-
 .../groovy/pages/role/CreateRolePage.groovy        |   6 +-
 .../groovy/pages/role/EditRolePage.groovy          |   7 +-
 .../groovy/pages/role/ListRolePage.groovy          |   9 +-
 .../groovy/pages/role/ShowRolePage.groovy          |   6 +-
 .../groovy/pages/user/CreateUserPage.groovy        |   9 +-
 .../groovy/pages/user/EditUserPage.groovy          |  12 +-
 .../groovy/pages/user/ListUserPage.groovy          |   9 +-
 .../groovy/specs/InheritanceSecuritySpec.groovy    |   3 +
 .../groovy/specs/RequestmapSpec.groovy             |  98 ++++++-------
 .../integration-test/groovy/specs/RoleSpec.groovy  |  90 ++++++------
 .../integration-test/groovy/specs/UserSpec.groovy  | 105 +++++++-------
 ...AutoConfigurationExcluderIntegrationSpec.groovy |  22 ---
 .../SecurityEventListenerSpec.groovy               |   7 +-
 .../springsecurity/SpringSecurityUtilsSpec.groovy  |   8 +-
 .../intercept/FilterSecurityInterceptorSpec.groovy |  71 ++++++++++
 .../AjaxAwareAuthenticationEntryPointSpec.groovy   |  18 +++
 .../MethodSecurityInterceptorSpec.groovy           | 147 +++++++++++++++++++
 ...FallbackMethodSecurityMetadataSourceSpec.groovy |  60 ++++++++
 ...ethodSecurityExpressionCompatibilitySpec.groovy | 155 +++++++++++++++++++++
 ...aultRestAuthenticationEventPublisherSpec.groovy |  18 +++
 .../groovy/page/AbstractSecurityPage.groovy        |   4 +
 .../groovy/spec/RegisterSpec.groovy                |   4 +-
 .../groovy/page/AbstractSecurityPage.groovy        |   4 +
 .../groovy/page/register/RegisterPage.groovy       |   1 -
 .../groovy/spec/RegisterSpec.groovy                |  70 ++++------
 57 files changed, 1086 insertions(+), 529 deletions(-)

diff --git a/plugin-acl/examples/functional-test-app/grails-app/controllers/com/testacl/ErrorsController.groovy b/plugin-acl/examples/functional-test-app/grails-app/controllers/com/testacl/ErrorsController.groovy
index 235400160..bc54ee072 100644
--- a/plugin-acl/examples/functional-test-app/grails-app/controllers/com/testacl/ErrorsController.groovy
+++ b/plugin-acl/examples/functional-test-app/grails-app/controllers/com/testacl/ErrorsController.groovy
@@ -27,7 +27,7 @@ import groovy.transform.CompileStatic
 class ErrorsController {
 
 	def error404() {
-		String uri = 'request.forwardURI'
+		String uri = request.forwardURI ?: request.requestURI ?: 'unknown URI'
 		if (!uri.contains('favicon.ico')) {
 			println "\n\nERROR 404: could not find $uri\n\n"
 		}
diff --git a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/AccessDeniedPage.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/AccessDeniedPage.groovy
new file mode 100644
index 000000000..fcf38042a
--- /dev/null
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/AccessDeniedPage.groovy
@@ -0,0 +1,8 @@
+package pages
+
+import geb.Page
+
+class AccessDeniedPage extends Page {
+
+    static at = { $('h1').text() == 'Access Denied' }
+}
diff --git a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ShowReportPage.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/DeleteReportPage.groovy
similarity index 84%
copy from plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ShowReportPage.groovy
copy to plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/DeleteReportPage.groovy
index f19bf4666..8d00b11da 100644
--- a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ShowReportPage.groovy
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/DeleteReportPage.groovy
@@ -19,13 +19,11 @@
 
 package pages
 
-class ShowReportPage extends ScaffoldPage {
+class DeleteReportPage extends ScaffoldPage {
 
-	static at = {
-		heading.text() == 'Show Report'
-	}
+	static url = '/report/delete'
 
-	static content = {
-		name { $('td#name').text() }
+	String convertToPath(Object[] args) {
+		args ? "?number=${args[0]}" : ''
 	}
 }
diff --git a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/EditReportPage.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/EditReportPage.groovy
index 8cf01042b..4d3d374a9 100644
--- a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/EditReportPage.groovy
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/EditReportPage.groovy
@@ -19,13 +19,22 @@
 
 package pages
 
+import geb.module.TextInput
+
 class EditReportPage extends ScaffoldPage {
 
+	static url = '/report/edit'
+
 	static at = {
-		heading.text() == 'Edit Report'
+		heading == 'Edit Report'
+	}
+
+	String convertToPath(Object[] args) {
+		args ? "?number=${args[0]}" : ''
 	}
 
 	static content = {
+		nameField { $('input', name: 'name').module(TextInput) }
 		updateButton { $('input', value: 'Update') }
 	}
 }
diff --git a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ListReportPage.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ListReportPage.groovy
index d0f508eb9..b6e4327dd 100644
--- a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ListReportPage.groovy
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ListReportPage.groovy
@@ -23,24 +23,44 @@ import geb.Module
 
 class ListReportPage extends ScaffoldPage {
 
-	static url = 'report/list?max=1000'
+	static url = 'report/list'
 
 	static at = {
 		title ==~ /Report List/
 	}
 
+	String convertToPath(Object[] args) {
+		if (!args) {
+			return ''
+		}
+
+		def params = args[0] as Map
+		if (!params) {
+			return ''
+		}
+
+		if (!params.containsKey('max')) {
+			params.max = 1000
+		}
+
+		'?' + params.collect { key, value ->
+			"${URLEncoder.encode(key.toString(), 'UTF-8')}=" +
+					"${URLEncoder.encode(value?.toString() ?: '', 'UTF-8')}"
+		}.join('&')
+	}
 	static content = {
+		message { $('div.message').text() }
+		nextLink { $('.nextLink') }
 		reportTable { $('div.list table', 0) }
-		reportRow { i -> module ReportRow, reportRows[i] }
-		reportRows(required: false) { reportTable.find('tbody').find('tr') }
+		reportRows { reportTable.find('tbody tr').moduleList(ReportRow) }
 	}
 }
 
 class ReportRow extends Module {
 	static content = {
-		cell { i -> $('td', i) }
-		cellText { i -> cell(i).text() }
-		cellHrefText{ i -> cell(i).find('a').text() }
+		cell { int i -> $('td', i) }
+		cellText { int i -> cell(i).text() }
+		cellHrefText { int i -> cell(i).find('a').text() }
 		name { cellText(1) }
 		showLink(to: ShowReportPage) { cell(0).find('a') }
 		grantLink(to: ReportGrantPage) { cell(2).find('a') }
diff --git a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/LoginPage.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/LoginPage.groovy
index 1120f3004..f40adf2fb 100644
--- a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/LoginPage.groovy
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/LoginPage.groovy
@@ -23,7 +23,7 @@ import geb.Page
 
 class LoginPage extends Page {
 
-	static url = 'login/auth'
+	static url = '/login/auth'
 
 	static at = { title == 'Login' }
 
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/CreatePage.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/LogoutPage.groovy
similarity index 91%
copy from plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/CreatePage.groovy
copy to plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/LogoutPage.groovy
index 39b1b9479..0f3a90cf2 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/CreatePage.groovy
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/LogoutPage.groovy
@@ -19,8 +19,9 @@
 
 package pages
 
-class CreatePage extends ScaffoldPage {
-	static at = {
-		title ==~ /Create.+/
-	}
+import geb.Page
+
+class LogoutPage extends Page {
+
+	static url = '/logout'
 }
diff --git a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ReportGrantPage.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ReportGrantPage.groovy
index c32c5e589..43ff38c53 100644
--- a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ReportGrantPage.groovy
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ReportGrantPage.groovy
@@ -19,13 +19,31 @@
 
 package pages
 
+import geb.module.TextInput
+
+import org.springframework.security.acls.model.Permission
+
 class ReportGrantPage extends ScaffoldPage {
 
+	static url = '/report/grant'
+
 	static at = {
-		heading.text() ==~ /Grant permission for.+/
+		heading ==~ /Grant permission for.+/
+	}
+
+	String convertToPath(Object[] args) {
+		args ? "?number=${args[0]}" : ''
 	}
 
 	static content = {
 		grantButton { $('input', value: 'Grant') }
+		permissionInput { $('input', name: 'permission').module(TextInput) }
+		recipientInput { $('input', name: 'recipient').module(TextInput) }
+	}
+
+	void grantPermission(String recipient, Permission permission) {
+		recipientInput.value(recipient)
+		permissionInput.value(permission.mask)
+		grantButton.click()
 	}
 }
diff --git a/plugin-ui/examples/simple/src/integration-test/groovy/page/AbstractSecurityPage.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ResetDataPage.groovy
similarity index 86%
copy from plugin-ui/examples/simple/src/integration-test/groovy/page/AbstractSecurityPage.groovy
copy to plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ResetDataPage.groovy
index 66151831c..eb2125847 100644
--- a/plugin-ui/examples/simple/src/integration-test/groovy/page/AbstractSecurityPage.groovy
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ResetDataPage.groovy
@@ -17,13 +17,15 @@
  *  under the License.
  */
 
-package page
+package pages
 
 import geb.Page
 
-abstract class AbstractSecurityPage extends Page {
+class ResetDataPage extends Page {
 
-	void submit() {
-		submitBtn.click()
-	}
+    static url = '/testData/reset'
+
+    static at = {
+        $().text() == 'OK'
+    }
 }
diff --git a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ScaffoldPage.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ScaffoldPage.groovy
index c402eedf1..4e6a5b492 100644
--- a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ScaffoldPage.groovy
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ScaffoldPage.groovy
@@ -24,7 +24,8 @@ import geb.Page
 class ScaffoldPage extends Page {
 
 	static content = {
-		heading { $('h1') }
+		h1 { $('h1', 0) }
+		heading { h1.text() }
 		message { $('div.message').text() }
 	}
 
diff --git a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ShowReportPage.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ShowReportPage.groovy
index f19bf4666..76b747a98 100644
--- a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ShowReportPage.groovy
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/pages/ShowReportPage.groovy
@@ -21,8 +21,14 @@ package pages
 
 class ShowReportPage extends ScaffoldPage {
 
+	static url = '/report/show'
+
 	static at = {
-		heading.text() == 'Show Report'
+		heading == 'Show Report'
+	}
+
+	String convertToPath(Object[] args) {
+		args ? "?number=${args[0]}" : ''
 	}
 
 	static content = {
diff --git a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/AbstractSecuritySpec.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/AbstractSecuritySpec.groovy
index b063da4ad..eeb6800f0 100644
--- a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/AbstractSecuritySpec.groovy
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/AbstractSecuritySpec.groovy
@@ -16,39 +16,42 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package test
 
+import pages.IndexPage
+import pages.ResetDataPage
+
 import grails.gorm.transactions.Rollback
 import grails.plugin.geb.ContainerGebSpec
-import grails.testing.mixin.integration.Integration
 import pages.LoginPage
+import pages.LogoutPage
 import spock.lang.Shared
 
 @Rollback
-@Integration
 abstract class AbstractSecuritySpec extends ContainerGebSpec {
 
 	@Shared boolean reset = false
 
 	void setup() {
 		if (!reset) {
-			go('testData/reset')
+			to(ResetDataPage)
 			reset = true
 		}
 		logout()
 	}
 
 	protected void login(String user) {
-		to(LoginPage).with {
+		via(LoginPage).with {
 			username = user
 			password = 'password'
 			loginButton.click()
 		}
+		at(IndexPage)
 	}
 
 	protected void logout() {
-		go('logout')
+		via(LogoutPage)
+		at(IndexPage)
 		clearCookies()
 	}
 }
diff --git a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/AdminFunctionalSpec.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/AdminFunctionalSpec.groovy
index 7c3e813cc..d5792696a 100644
--- a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/AdminFunctionalSpec.groovy
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/AdminFunctionalSpec.groovy
@@ -16,20 +16,20 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package test
 
-import grails.testing.mixin.integration.Integration
-import org.springframework.security.acls.domain.BasePermission
-
+import pages.DeleteReportPage
 import pages.EditReportPage
 import pages.IndexPage
 import pages.ListReportPage
 import pages.ReportGrantPage
 import pages.ShowReportPage
-import spock.lang.Stepwise
+import spock.lang.Unroll
+
+import grails.testing.mixin.integration.Integration
+
+import static org.springframework.security.acls.domain.BasePermission.READ
 
-@Stepwise
 @Integration
 class AdminFunctionalSpec extends AbstractSecuritySpec {
 
@@ -41,54 +41,55 @@ class AdminFunctionalSpec extends AbstractSecuritySpec {
 
 	void 'check tags'() {
 		when:
-		go('tagLibTest/test')
+		go('/tagLibTest/test')
 
 		then:
-		pageSource.contains('test 1 true 1')
-		pageSource.contains('test 2 true 1')
-		pageSource.contains('test 3 true 1')
-		pageSource.contains('test 4 true 1')
-		pageSource.contains('test 5 true 1')
-		pageSource.contains('test 6 true 1')
-
-		pageSource.contains('test 1 true 13')
-		pageSource.contains('test 2 true 13')
-		pageSource.contains('test 3 true 13')
-		pageSource.contains('test 4 true 13')
-		pageSource.contains('test 5 true 13')
-		pageSource.contains('test 6 true 13')
-
-		pageSource.contains('test 1 true 80')
-		pageSource.contains('test 2 true 80')
-		pageSource.contains('test 3 true 80')
-		pageSource.contains('test 4 true 80')
-		pageSource.contains('test 5 true 80')
-		pageSource.contains('test 6 true 80')
+		with(pageSource) {
+			contains('test 1 true 1')
+			contains('test 2 true 1')
+			contains('test 3 true 1')
+			contains('test 4 true 1')
+			contains('test 5 true 1')
+			contains('test 6 true 1')
+
+			contains('test 1 true 13')
+			contains('test 2 true 13')
+			contains('test 3 true 13')
+			contains('test 4 true 13')
+			contains('test 5 true 13')
+			contains('test 6 true 13')
+
+			contains('test 1 true 80')
+			contains('test 2 true 80')
+			contains('test 3 true 80')
+			contains('test 4 true 80')
+			contains('test 5 true 80')
+			contains('test 6 true 80')
+		}
 	}
 
+	@Unroll
 	void 'view all'() {
 		when:
-		go("report/show?number=$i")
+		def page = to(ShowReportPage, i)
 
 		then:
-		pageSource.contains("report$i")
+		page.name == "report$i"
 
 		where:
 		i << (1..100)
 	}
 
 	void 'edit report 15'() {
-
 		when:
-		go('report/edit?number=15')
+		def page = to(EditReportPage, 15)
 
 		then:
-		at(EditReportPage)
-		$('form').name == 'report15'
+		page.nameField.text == 'report15'
 
 		when:
-		name = 'report15_new'
-		updateButton.click()
+		page.nameField = 'report15_new'
+		page.updateButton.click()
 
 		then:
 		at(ShowReportPage)
@@ -97,34 +98,33 @@ class AdminFunctionalSpec extends AbstractSecuritySpec {
 
 	void 'delete report 15'() {
 		when:
-		go('report/delete?number=15')
-		def listReportPage = at(ListReportPage)
+		via(DeleteReportPage, 15)
 
 		then:
-		message == 'Report 15 deleted'
-		listReportPage.reportRows.size() == 99
+		def page = at(ListReportPage)
+
+		and:
+		page.message == 'Report 15 deleted'
+		page.reportRows.size() == 99
 	}
 
 	void 'grant edit 16'() {
 		when:
-		go('report/grant?number=16')
-		def reportGrantPage = at(ReportGrantPage)
+		def page = to(ReportGrantPage, 16)
 
 		then:
-		pageSource.contains('Grant permission for report16')
+		page.heading == 'Grant permission for report16'
 
 		when:
-		recipient = 'user2'
-		permission = BasePermission.READ.mask.toString()
-		reportGrantPage.grantButton.click()
-		at(ShowReportPage)
+		page.grantPermission('user2', READ)
+		page = at(ShowReportPage)
 
 		then:
-		pageSource.contains("Permission $BasePermission.READ.mask granted on Report 16 to user2")
+		page.message == "Permission $READ.mask granted on Report 16 to user2"
 
 		// login as user2 and verify the grant
 		when:
-		go('logout')
+		logout()
 
 		then:
 		at(IndexPage)
@@ -132,13 +132,10 @@ class AdminFunctionalSpec extends AbstractSecuritySpec {
 		when:
 		login('user2')
 
-		then:
-		at(IndexPage)
-
-		when:
-		go('report/show?number=16')
+		and:
+		page = to(ShowReportPage, 16)
 
 		then:
-		pageSource.contains('report16')
+		page.name == 'report16'
 	}
 }
diff --git a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/User1FunctionalSpec.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/User1FunctionalSpec.groovy
index 73a67aaa3..c62b204ad 100644
--- a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/User1FunctionalSpec.groovy
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/User1FunctionalSpec.groovy
@@ -16,19 +16,21 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package test
 
-import grails.testing.mixin.integration.Integration
-import org.springframework.security.acls.domain.BasePermission
-
+import pages.AccessDeniedPage
+import pages.DeleteReportPage
 import pages.EditReportPage
 import pages.ListReportPage
 import pages.ReportGrantPage
 import pages.ShowReportPage
-import spock.lang.Stepwise
+import spock.lang.Unroll
+
+import grails.testing.mixin.integration.Integration
+
+import static org.springframework.security.acls.domain.BasePermission.READ
+import static org.springframework.security.acls.domain.BasePermission.WRITE
 
-@Stepwise
 @Integration
 class User1FunctionalSpec extends AbstractSecuritySpec {
 
@@ -40,144 +42,136 @@ class User1FunctionalSpec extends AbstractSecuritySpec {
 
 	void 'check tags'() {
 		when:
-		go('tagLibTest/test')
+		go('/tagLibTest/test')
 
 		then:
-		pageSource.contains('test 1 true 1')
-		pageSource.contains('test 2 true 1')
-		pageSource.contains('test 3 true 1')
-		pageSource.contains('test 4 true 1')
-		pageSource.contains('test 5 true 1')
-		pageSource.contains('test 6 true 1')
-
-		pageSource.contains('test 1 true 13')
-		pageSource.contains('test 2 true 13')
-		pageSource.contains('test 3 true 13')
-		pageSource.contains('test 4 true 13')
-		pageSource.contains('test 5 true 13')
-		pageSource.contains('test 6 true 13')
-
-		pageSource.contains('test 1 false 80')
-		pageSource.contains('test 2 false 80')
-		pageSource.contains('test 3 false 80')
-		pageSource.contains('test 4 false 80')
-		pageSource.contains('test 5 false 80')
-		pageSource.contains('test 6 false 80')
+		with(pageSource) {
+			contains('test 1 true 1')
+			contains('test 2 true 1')
+			contains('test 3 true 1')
+			contains('test 4 true 1')
+			contains('test 5 true 1')
+			contains('test 6 true 1')
+
+			contains('test 1 true 13')
+			contains('test 2 true 13')
+			contains('test 3 true 13')
+			contains('test 4 true 13')
+			contains('test 5 true 13')
+			contains('test 6 true 13')
+
+			contains('test 1 false 80')
+			contains('test 2 false 80')
+			contains('test 3 false 80')
+			contains('test 4 false 80')
+			contains('test 5 false 80')
+			contains('test 6 false 80')
+		}
 	}
 
+	@Unroll
 	void 'view all (1-67)'() {
 		when:
-		go("report/show?number=$i")
+		def page = to(ShowReportPage, i)
 
 		then:
-		pageSource.contains("report$i")
+		page.name == "report$i"
 
 		where:
 		i << (1..67)
 	}
 
+	@Unroll
 	void 'view all (68-100)'() {
 		when:
-		go("report/show?number=$i")
+		via(ShowReportPage, i)
 
 		then:
-		pageSource.contains('Access Denied')
+		at(AccessDeniedPage)
 
 		where:
-		i << (68..100)
+		i << (68..69)
 	}
 
 	void 'edit report 11'() {
 		when:
-		go('report/edit?number=11')
-		def editPage = at(EditReportPage)
+		def page = to(EditReportPage, 11)
 
 		then:
-		$('form').name == 'report11'
+		page.nameField.text == 'report11'
 
 		when:
-		name = 'report11_new'
-		editPage.updateButton.click()
+		page.nameField.text = 'report11_new'
+		page.updateButton.click()
+		page = at(ShowReportPage)
 
 		then:
-		at(ShowReportPage)
-		pageSource.contains('report11_new')
+		page.name == 'report11_new'
 	}
 
 	void 'delete report 11'() {
 		when:
-		go('report/delete?number=11')
-		def listPage = at(ListReportPage)
+		via(DeleteReportPage, 11)
+		def page = at(ListReportPage)
 
 		then:
-		message == 'Report 11 deleted'
-		listPage.reportRows.size() == 66
+		page.message == 'Report 11 deleted'
+		page.reportRows.size() == 66
 	}
 
 	void 'grant edit 12'() {
 		when:
-		go('report/grant?number=12')
-		def grantPage = at(ReportGrantPage)
+		def page = to(ReportGrantPage, 12)
 
 		then:
-		pageSource.contains('Grant permission for report12')
+		page.heading == 'Grant permission for report12'
 
 		when:
-		recipient = 'user2'
-		permission = BasePermission.READ.mask.toString()
-		grantPage.grantButton.click()
+		page.grantPermission('user2', READ)
 
 		then:
-		at(ShowReportPage)
-		pageSource.contains("Permission $BasePermission.READ.mask granted on Report 12 to user2")
+		page.message == "Permission $READ.mask granted on Report 12 to user2"
 
 		when:
-		go('report/grant?number=12')
-		grantPage = at(ReportGrantPage)
+		to(ReportGrantPage, 12)
 
 		then:
-		pageSource.contains('Grant permission for report12')
+		page.heading == 'Grant permission for report12'
 
 		when:
-		recipient = 'user2'
-		permission = BasePermission.WRITE.mask.toString()
-		grantPage.grantButton.click()
+		page.grantPermission('user2', WRITE)
 
 		then:
 		at(ShowReportPage)
-		pageSource.contains("Permission $BasePermission.WRITE.mask granted on Report 12 to user2")
+		page.message == "Permission $WRITE.mask granted on Report 12 to user2"
 	}
 
 	void 'grant edit 13'() {
 		when:
-		go('report/grant?number=13')
-		def grantPage = at(ReportGrantPage)
+		def page = to(ReportGrantPage, 13)
 
 		then:
-		pageSource.contains('Grant permission for report13')
+		page.heading == 'Grant permission for report13'
 
 		when:
-		recipient = 'user2'
-		permission = BasePermission.WRITE.mask.toString()
-		grantPage.grantButton.click()
+		page.grantPermission('user2', WRITE)
 
 		then:
-		pageSource.contains('Access Denied')
+		at(AccessDeniedPage)
 	}
 
 	void 'edit report 20'() {
 		when:
-		go('report/edit?number=20')
-		def editPage = at(EditReportPage)
+		def page = to(EditReportPage, 20)
 
 		then:
-		$('form').name == 'report20'
+		page.nameField.text == 'report20'
 
 		when:
-		name = 'report20_new'
-		editPage.updateButton.click()
+		page.nameField.text = 'report20_new'
+		page.updateButton.click()
 
 		then:
-		pageSource.contains('Access Denied')
+		at(AccessDeniedPage)
 	}
 }
diff --git a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/User2FunctionalSpec.groovy b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/User2FunctionalSpec.groovy
index bda6c2330..c2803dcaa 100644
--- a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/User2FunctionalSpec.groovy
+++ b/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/User2FunctionalSpec.groovy
@@ -16,17 +16,20 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package test
 
-import grails.testing.mixin.integration.Integration
-import org.springframework.security.acls.domain.BasePermission
+import pages.AccessDeniedPage
+import pages.DeleteReportPage
 import pages.EditReportPage
+import pages.ListReportPage
 import pages.ReportGrantPage
 import pages.ShowReportPage
-import spock.lang.Stepwise
+import spock.lang.Unroll
+
+import grails.testing.mixin.integration.Integration
+
+import static org.springframework.security.acls.domain.BasePermission.WRITE
 
-@Stepwise
 @Integration
 class User2FunctionalSpec extends AbstractSecuritySpec {
 
@@ -36,120 +39,118 @@ class User2FunctionalSpec extends AbstractSecuritySpec {
 		login('user2')
 	}
 
+	@Unroll
 	void 'view all (1-5)'() {
 		when:
-		go("report/show?number=$i")
+		def page = to(ShowReportPage, 1)
 
 		then:
-		pageSource.contains("report$i")
+		page.name == 'report1'
 
 		where:
 		i << (1..5)
 	}
 
+	@Unroll
 	void 'view all (6-100)'() {
 		when:
-		go("report/show?number=$i")
+		via(ShowReportPage, i)
 
 		then:
-		pageSource.contains('Access Denied')
+		at(AccessDeniedPage)
 
 		where:
 		i << (6..100)
 	}
 
 	void 'edit report 11'() {
-
 		when:
-		go('report/edit?number=11')
+		via(EditReportPage, 11)
 
 		then:
-		pageSource.contains('Access Denied')
+		at(AccessDeniedPage)
 	}
 
 	void 'delete report 1'() {
 		when:
-		go('report/delete?number=1')
+		via(DeleteReportPage, 1)
 
 		then:
-		pageSource.contains('Access Denied')
+		at(AccessDeniedPage)
 	}
 
 	void 'grant edit 2'() {
 		when:
-		go('report/grant?number=2')
-		def grantPage = at(ReportGrantPage)
+		def page = to(ReportGrantPage, 2)
 
 		then:
-		pageSource.contains('Grant permission for report2')
+		page.heading == 'Grant permission for report2'
 
 		when:
-		recipient = 'user1'
-		permission = BasePermission.WRITE.mask.toString()
-		grantPage.grantButton.click()
+		page.grantPermission('user1', WRITE)
 
 		then:
-		pageSource.contains('Access Denied')
+		at(AccessDeniedPage)
 	}
 
 	void 'edit report 5'() {
 		when:
-		go('report/edit?number=5')
-		def editPage = at(EditReportPage)
+		def page = to(EditReportPage, 5)
 
 		then:
-		$('form').name == 'report5'
+		page.nameField.text == 'report5'
 
 		when:
-		name = 'report5_new'
-		editPage.updateButton.click()
+		page.nameField.text = 'report5_new'
+		page.updateButton.click()
+		page = at(ShowReportPage)
 
 		then:
-		at(ShowReportPage)
-		pageSource.contains('report5_new')
+		page.name == 'report5_new'
 	}
 
 	void 'list is filtered'() {
-
 		when:
-		go('report/list')
+		def page = to(ListReportPage)
 
 		then:
-		pageSource.contains('report1')
-		!pageSource.contains('report6')
+		page.reportRows[0].name == 'report1'
+		page.reportRows.every {it.name != 'report6' }
 
 		when:
-		go('report/list?offset=80&max=10')
+		to(ListReportPage, [offset: 80, max: 10])
 
 		then:
-		pageSource.contains('Next')
-		!pageSource.contains('report85')
+		page.nextLink.displayed
+		page.reportRows.every {it.name != 'report85' }
 	}
 
 	void 'check tags'() {
 		when:
-		go('tagLibTest/test')
+		go('/tagLibTest/test')
 
 		then:
-		pageSource.contains('test 1 true 1')
-		pageSource.contains('test 2 true 1')
-		pageSource.contains('test 3 true 1')
-		pageSource.contains('test 4 true 1')
-		pageSource.contains('test 5 true 1')
-		pageSource.contains('test 6 true 1')
-
-		pageSource.contains('test 1 false 13')
-		pageSource.contains('test 2 false 13')
-		pageSource.contains('test 3 false 13')
-		pageSource.contains('test 4 false 13')
-		pageSource.contains('test 5 false 13')
-		pageSource.contains('test 6 false 13')
-
-		pageSource.contains('test 1 false 80')
-		pageSource.contains('test 2 false 80')
-		pageSource.contains('test 3 false 80')
-		pageSource.contains('test 4 false 80')
-		pageSource.contains('test 5 false 80')
-		pageSource.contains('test 6 false 80')
+		with(pageSource) {
+			contains('test 1 true 1')
+			contains('test 2 true 1')
+			contains('test 3 true 1')
+			contains('test 4 true 1')
+			contains('test 5 true 1')
+			contains('test 6 true 1')
+
+			contains('test 1 false 13')
+			contains('test 2 false 13')
+			contains('test 3 false 13')
+			contains('test 4 false 13')
+			contains('test 5 false 13')
+			contains('test 6 false 13')
+
+			contains('test 1 false 80')
+			contains('test 2 false 80')
+			contains('test 3 false 80')
+			contains('test 4 false 80')
+			contains('test 5 false 80')
+			contains('test 6 false 80')
+		}
 	}
 }
diff --git a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/AbstractSecuritySpec.groovy b/plugin-acl/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/acl/TestClassAnnotatedServiceMetadataSpec.groovy
similarity index 55%
copy from plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/AbstractSecuritySpec.groovy
copy to plugin-acl/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/acl/TestClassAnnotatedServiceMetadataSpec.groovy
index b063da4ad..ec5b13e4b 100644
--- a/plugin-acl/examples/functional-test-app/src/integration-test/groovy/test/AbstractSecuritySpec.groovy
+++ b/plugin-acl/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/acl/TestClassAnnotatedServiceMetadataSpec.groovy
@@ -16,39 +16,30 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
+package grails.plugin.springsecurity.acl
 
-package test
-
-import grails.gorm.transactions.Rollback
-import grails.plugin.geb.ContainerGebSpec
+import grails.util.Holders
 import grails.testing.mixin.integration.Integration
-import pages.LoginPage
-import spock.lang.Shared
+import grails.gorm.transactions.Rollback
+import org.springframework.security.access.method.MethodSecurityMetadataSource
 
-@Rollback
 @Integration
-abstract class AbstractSecuritySpec extends ContainerGebSpec {
+@Rollback
+class TestClassAnnotatedServiceMetadataSpec extends AbstractIntegrationSpec {
 
-	@Shared boolean reset = false
+	void 'userAnnotated metadata resolves from the live proxied bean'() {
+		given:
+		def applicationContext = Holders.grailsApplication.mainContext
+		MethodSecurityMetadataSource aclSecurityMetadataSource = applicationContext.getBean('aclSecurityMetadataSource', MethodSecurityMetadataSource)
+		def beanType = applicationContext.getType('testClassAnnotatedService')
+		def beanTypeMethod = beanType.getMethod('userAnnotated')
 
-	void setup() {
-		if (!reset) {
-			go('testData/reset')
-			reset = true
-		}
-		logout()
+		expect:
+		aclSecurityMetadataSource.getAttributes(beanTypeMethod, beanType)*.attribute == ['ROLE_USER']
 	}
+}
+
+
+
 
-	protected void login(String user) {
-		to(LoginPage).with {
-			username = user
-			password = 'password'
-			loginButton.click()
-		}
-	}
 
-	protected void logout() {
-		go('logout')
-		clearCookies()
-	}
-}
diff --git a/plugin-acl/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/acl/TestClassAnnotatedServiceSpec.groovy b/plugin-acl/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/acl/TestClassAnnotatedServiceSpec.groovy
index 4fa12c111..ac0d6ee5d 100644
--- a/plugin-acl/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/acl/TestClassAnnotatedServiceSpec.groovy
+++ b/plugin-acl/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/acl/TestClassAnnotatedServiceSpec.groovy
@@ -56,10 +56,10 @@ class TestClassAnnotatedServiceSpec extends AbstractAclSpec {
 		testClassAnnotatedService.notAnnotated()
 
 		then:
-		notThrown()
+		noExceptionThrown()
 	}
 
-	void 'check that the userAnnotated method overides the class annotation and requires ROLE_USER'() {
+	void 'check that the userAnnotated method overides the class annotation and requires authentication'() {
 		given:
 		buildReports()
 
@@ -69,18 +69,29 @@ class TestClassAnnotatedServiceSpec extends AbstractAclSpec {
 		then:
 		thrown AuthenticationCredentialsNotFoundException
 
-		when:
+	}
+
+	void 'check that the userAnnotated method overrides the class annotation and denies ROLE_ADMIN'() {
+		given:
+		buildReports()
 		authenticateAsAdmin()
+
+		when:
 		testClassAnnotatedService.userAnnotated()
 
 		then:
 		thrown AccessDeniedException
+	}
 
-		when:
+	void 'check that the userAnnotated method overrides the class annotation and allows ROLE_USER'() {
+		given:
+		buildReports()
 		authenticateAsUser()
+
+		when:
 		testClassAnnotatedService.userAnnotated()
 
 		then:
-		notThrown()
+		noExceptionThrown()
 	}
 }
diff --git a/plugin-acl/plugin/src/test/groovy/grails/plugin/springsecurity/acl/access/method/SecuredAnnotationSecurityMetadataSourceSpec.groovy b/plugin-acl/plugin/src/test/groovy/grails/plugin/springsecurity/acl/access/method/SecuredAnnotationSecurityMetadataSourceSpec.groovy
new file mode 100644
index 000000000..0b928ff66
--- /dev/null
+++ b/plugin-acl/plugin/src/test/groovy/grails/plugin/springsecurity/acl/access/method/SecuredAnnotationSecurityMetadataSourceSpec.groovy
@@ -0,0 +1,58 @@
+/*
+ *  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 grails.plugin.springsecurity.acl.access.method
+
+import grails.plugin.springsecurity.annotation.Secured
+import spock.lang.Specification
+
+class SecuredAnnotationSecurityMetadataSourceSpec extends Specification {
+
+	private SecuredAnnotationSecurityMetadataSource metadataSource = new SecuredAnnotationSecurityMetadataSource(
+		serviceClassNames: [AnnotatedService.name]
+	)
+
+	void 'getAttributes resolves method annotations for proxied or subclassed service targets'() {
+		given:
+		def method = ProxiedAnnotatedService.getMethod('userAnnotated')
+
+		expect:
+		metadataSource.getAttributes(method, ProxiedAnnotatedService)*.attribute == ['ROLE_USER']
+	}
+
+	void 'getAttributes falls back to inherited class annotations for proxied or subclassed service targets'() {
+		given:
+		def method = ProxiedAnnotatedService.getMethod('notAnnotated')
+
+		expect:
+		metadataSource.getAttributes(method, ProxiedAnnotatedService)*.attribute == ['ROLE_ADMIN']
+	}
+
+	@Secured('ROLE_ADMIN')
+	private static class AnnotatedService {
+
+		void notAnnotated() {}
+
+		@Secured('ROLE_USER')
+		void userAnnotated() {}
+	}
+
+	private static class ProxiedAnnotatedService extends AnnotatedService {
+	}
+}
+
diff --git a/plugin-core/examples/functional-test-app/grails-app/views/testRequestmap/edit.gsp b/plugin-core/examples/functional-test-app/grails-app/views/testRequestmap/edit.gsp
index 31c5a9948..1c0c4f0e3 100644
--- a/plugin-core/examples/functional-test-app/grails-app/views/testRequestmap/edit.gsp
+++ b/plugin-core/examples/functional-test-app/grails-app/views/testRequestmap/edit.gsp
@@ -40,13 +40,14 @@
 				</g:eachError>
 			</ul>
 			</g:hasErrors>
-			<g:form url="[resource:testRequestmap, action:'update']" method="PUT" >
+			<g:form url="[resource: testRequestmap, action: 'update']" method="PUT">
+				<g:hiddenField name="id" value="${testRequestmap?.id}" />
 				<g:hiddenField name="version" value="${testRequestmap?.version}" />
 				<fieldset class="form">
 					<g:render template="form"/>
 				</fieldset>
 				<fieldset class="buttons">
-					<g:actionSubmit class="save" action="update" value='Update' />
+					<input type="submit" class="save" value='Update'>
 				</fieldset>
 			</g:form>
 		</div>
diff --git a/plugin-core/examples/functional-test-app/grails-app/views/testRequestmap/show.gsp b/plugin-core/examples/functional-test-app/grails-app/views/testRequestmap/show.gsp
index 80c23995e..8bd1ce5fd 100644
--- a/plugin-core/examples/functional-test-app/grails-app/views/testRequestmap/show.gsp
+++ b/plugin-core/examples/functional-test-app/grails-app/views/testRequestmap/show.gsp
@@ -53,10 +53,10 @@
 				</li>
 				</g:if>
 			</ol>
-			<g:form url="[resource:testRequestmap, action:'delete']" method="DELETE">
+			<g:form url="[resource: testRequestmap, action: 'delete']" method="DELETE" onsubmit="return confirm('Are you sure?');">
 				<fieldset class="buttons">
 					<g:link class="edit" action="edit" resource="${testRequestmap}">Edit</g:link>
-					<g:actionSubmit class="delete" action="delete" value='Delete' onclick="return confirm('Are you sure?');" />
+					<input type="submit" class="delete" value="Delete">
 				</fieldset>
 			</g:form>
 		</div>
diff --git a/plugin-core/examples/functional-test-app/grails-app/views/testRole/create.gsp b/plugin-core/examples/functional-test-app/grails-app/views/testRole/create.gsp
index 101eafad1..57fe9cb30 100644
--- a/plugin-core/examples/functional-test-app/grails-app/views/testRole/create.gsp
+++ b/plugin-core/examples/functional-test-app/grails-app/views/testRole/create.gsp
@@ -39,12 +39,12 @@
 				</g:eachError>
 			</ul>
 			</g:hasErrors>
-			<g:form url="[resource:testRole, action:'save']" >
+			<g:form url="[resource: testRole, action: 'save']">
 				<fieldset class="form">
 					<g:render template="form"/>
 				</fieldset>
 				<fieldset class="buttons">
-					<g:submitButton name="create" class="save" value='Create' />
+					<input type="submit" name="create" class="save" value="Create">
 				</fieldset>
 			</g:form>
 		</div>
diff --git a/plugin-core/examples/functional-test-app/grails-app/views/testRole/edit.gsp b/plugin-core/examples/functional-test-app/grails-app/views/testRole/edit.gsp
index 7c514f2d1..ff434df59 100644
--- a/plugin-core/examples/functional-test-app/grails-app/views/testRole/edit.gsp
+++ b/plugin-core/examples/functional-test-app/grails-app/views/testRole/edit.gsp
@@ -40,13 +40,13 @@
 				</g:eachError>
 			</ul>
 			</g:hasErrors>
-			<g:form url="[resource:testRole, action:'update']" method="PUT" >
+			<g:form url="[resource: testRole, action: 'update']" method="PUT">
 				<g:hiddenField name="version" value="${testRole?.version}" />
 				<fieldset class="form">
 					<g:render template="form"/>
 				</fieldset>
 				<fieldset class="buttons">
-					<g:actionSubmit class="save" action="update" value='Update' />
+					<input type="submit" class="save" value="Update">
 				</fieldset>
 			</g:form>
 		</div>
diff --git a/plugin-core/examples/functional-test-app/grails-app/views/testRole/show.gsp b/plugin-core/examples/functional-test-app/grails-app/views/testRole/show.gsp
index aab053524..35760c803 100644
--- a/plugin-core/examples/functional-test-app/grails-app/views/testRole/show.gsp
+++ b/plugin-core/examples/functional-test-app/grails-app/views/testRole/show.gsp
@@ -41,10 +41,10 @@
 				</li>
 				</g:if>
 			</ol>
-			<g:form url="[resource:testRole, action:'delete']" method="DELETE">
+			<g:form url="[resource: testRole, action: 'delete']" method="DELETE" onsubmit="return confirm('Are you sure?');">
 				<fieldset class="buttons">
 					<g:link class="edit" action="edit" resource="${testRole}">Edit</g:link>
-					<g:actionSubmit class="delete" action="delete" value='Delete' onclick="return confirm('Are you sure?');" />
+					<input type="submit" class="delete" value="Delete">
 				</fieldset>
 			</g:form>
 		</div>
diff --git a/plugin-core/examples/functional-test-app/grails-app/views/testUser/edit.gsp b/plugin-core/examples/functional-test-app/grails-app/views/testUser/edit.gsp
index 1667561b3..86f1a38d8 100644
--- a/plugin-core/examples/functional-test-app/grails-app/views/testUser/edit.gsp
+++ b/plugin-core/examples/functional-test-app/grails-app/views/testUser/edit.gsp
@@ -94,8 +94,8 @@
 		</div>
 
 		<div class="buttons">
-			<span class="button"><g:actionSubmit class="save" action="update" value='Update' /></span>
-			<span class="button"><g:actionSubmit class="delete" action="delete" value='Delete' onclick="return confirm('Are you sure?');" /></span>
+			<span class="button"><g:formActionSubmit action="update" method="PUT" class="save" value='Update' /></span>
+			<span class="button"><g:formActionSubmit action="delete" method="DELETE" class="delete" value='Delete' onclick="return confirm('Are you sure?');" /></span>
 		</div>
 	</g:form>
 </div>
diff --git a/plugin-core/examples/functional-test-app/grails-app/views/testUser/show.gsp b/plugin-core/examples/functional-test-app/grails-app/views/testUser/show.gsp
index a05f70aef..b6d44808b 100644
--- a/plugin-core/examples/functional-test-app/grails-app/views/testUser/show.gsp
+++ b/plugin-core/examples/functional-test-app/grails-app/views/testUser/show.gsp
@@ -75,7 +75,7 @@
 			<fieldset class="buttons">
 				<g:hiddenField name="id" value="${person?.id}" />
 				<g:link class="edit" action="edit" id="${person?.id}">Edit</g:link>
-				<g:actionSubmit class="delete" action="delete" value='Delete' onclick="return confirm('Are you sure?');" />
+				<g:formActionSubmit action="delete" method="DELETE" class="delete" value='Delete' onclick="return confirm('Are you sure?');" />
 			</fieldset>
 		</g:form>
 	</div>
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/CreatePage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/CreatePage.groovy
index 39b1b9479..e1c36a1db 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/CreatePage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/CreatePage.groovy
@@ -16,10 +16,10 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package pages
 
 class CreatePage extends ScaffoldPage {
+
 	static at = {
 		title ==~ /Create.+/
 	}
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/EditPage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/EditPage.groovy
index 8863422b3..293bd4d4d 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/EditPage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/EditPage.groovy
@@ -16,11 +16,11 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package pages
 
 class EditPage extends ScaffoldPage {
+
 	static at = {
-		heading.text() ==~ /Edit.+/
+		heading ==~ /Edit.+/
 	}
 }
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/ScaffoldPage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/ScaffoldPage.groovy
index 4b04eba1b..e30d64e6f 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/ScaffoldPage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/ScaffoldPage.groovy
@@ -16,14 +16,15 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package pages
 
 import geb.Page
 
 class ScaffoldPage extends Page {
+
 	static content = {
-		heading { $('h1') }
+		h1 { $('h1') }
+		heading { h1.text() }
 		message { $('div.message').text() }
 	}
 
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/ShowPage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/ShowPage.groovy
index c6b96296a..db55d0487 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/ShowPage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/ShowPage.groovy
@@ -16,11 +16,11 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package pages
 
 class ShowPage extends ScaffoldPage {
+
 	static at = {
-		heading.text() ==~ /Show .+/
+		heading ==~ /Show .+/
 	}
 }
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/requestmap/CreateRequestmapPage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/requestmap/CreateRequestmapPage.groovy
index f6025b8f6..39f21ae1d 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/requestmap/CreateRequestmapPage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/requestmap/CreateRequestmapPage.groovy
@@ -16,13 +16,16 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package pages.requestmap
 
+import geb.module.TextInput
 import pages.CreatePage
 
 class CreateRequestmapPage extends CreatePage {
+
 	static content = {
-		createButton(to: ShowRequestmapPage) { create() }
+		urlField { $('input', name: 'url').module(TextInput) }
+		configAttributeField { $('input', name: 'configAttribute').module(TextInput) }
+		createButton(to: ShowRequestmapPage) { $('input', type: 'submit') }
 	}
 }
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/requestmap/EditRequestmapPage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/requestmap/EditRequestmapPage.groovy
index c8d81fd1d..55a6a6070 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/requestmap/EditRequestmapPage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/requestmap/EditRequestmapPage.groovy
@@ -16,13 +16,16 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package pages.requestmap
 
+import geb.module.TextInput
 import pages.EditPage
 
 class EditRequestmapPage extends EditPage {
+
 	static content = {
+		urlField { $('input', name: 'url').module(TextInput) }
+		configAttributeField { $('input', name: 'configAttribute').module(TextInput) }
 		updateButton(to: ShowRequestmapPage) { $('input', value: 'Update') }
 		deleteButton(to: ListRequestmapPage) { $('input', value: 'Delete') }
 	}
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/requestmap/ListRequestmapPage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/requestmap/ListRequestmapPage.groovy
index ba4404ad8..d8d4ca8b2 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/requestmap/ListRequestmapPage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/requestmap/ListRequestmapPage.groovy
@@ -34,15 +34,15 @@ class ListRequestmapPage extends ScaffoldPage {
 		newRequestmapButton(to: CreateRequestmapPage) { $('a', text: 'New TestRequestmap') }
 		requestmapTable { $('div.content table', 0) }
 		requestmapRows(required: false) { requestmapTable.find('tbody').find('tr') }
-		requestmapRow { i -> requestmapRows[i].module(RequestmapRow) }
+		requestmapRow { int i -> requestmapRows[i].module(RequestmapRow) }
 	}
 }
 
 class RequestmapRow extends Module {
 	static content = {
-		cell { i -> $('td', i) }
-		cellText { i -> cell(i).text() }
-		cellHrefText{ i -> cell(i).find('a').text() }
+		cell { int i -> $('td', i) }
+		cellText { int i -> cell(i).text() }
+		cellHrefText { int i -> cell(i).find('a').text() }
 
 		configAttribute { cellText(1) }
 		showLink(to: ShowRequestmapPage) { cell(0).find('a') }
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/CreateRolePage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/CreateRolePage.groovy
index 01ae34603..1ecedaab1 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/CreateRolePage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/CreateRolePage.groovy
@@ -16,13 +16,15 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package pages.role
 
+import geb.module.TextInput
 import pages.CreatePage
 
 class CreateRolePage extends CreatePage {
+
 	static content = {
-		createButton(to: ShowRolePage) { create() }
+		authorityField { $('input', name: 'authority').module(TextInput) }
+		createButton(to: ShowRolePage) { $('input', name: 'create') }
 	}
 }
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/EditRolePage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/EditRolePage.groovy
index 641ec8b00..b5b1029ea 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/EditRolePage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/EditRolePage.groovy
@@ -16,14 +16,15 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package pages.role
 
+import geb.module.TextInput
 import pages.EditPage
 
 class EditRolePage extends EditPage {
+
 	static content = {
-		updateButton(to: ShowRolePage) { $('input', value: 'Update') }
-		deleteButton(to: ListRolePage) { $('input', value: 'Delete') }
+		authorityField { $('input', name: 'authority').module(TextInput) }
+		updateButton { $('input', value: 'Update') }
 	}
 }
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/ListRolePage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/ListRolePage.groovy
index cb525ce10..ca2272d24 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/ListRolePage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/ListRolePage.groovy
@@ -16,7 +16,6 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package pages.role
 
 import geb.Module
@@ -33,16 +32,16 @@ class ListRolePage extends ScaffoldPage {
 	static content = {
 		newRoleButton(to: CreateRolePage) { $('a', text: 'New TestRole') }
 		roleTable { $('div.content table', 0) }
-		roleRow { i -> roleRows[i].module RoleRow }
+		roleRow { int i -> roleRows[i].module(RoleRow) }
 		roleRows(required: false) { roleTable.find('tbody').find('tr') }
 	}
 }
 
 class RoleRow extends Module {
 	static content = {
-		cell { i -> $('td', i) }
-		cellText { i -> cell(i).text() }
-		cellHrefText{ i -> cell(i).find('a').text() }
+		cell { int i -> $('td', i) }
+		cellText { int i -> cell(i).text() }
+		cellHrefText { int i -> cell(i).find('a').text() }
 		authority { cellText(0) }
 		showLink(to: ShowRolePage) { cell(0).find('a') }
 	}
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/ShowRolePage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/ShowRolePage.groovy
index 468a662d3..1cfdd7c66 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/ShowRolePage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/role/ShowRolePage.groovy
@@ -16,15 +16,15 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package pages.role
 
 import pages.ShowPage
 
 class ShowRolePage extends ShowPage {
+
 	static content = {
-		editButton(to: EditRolePage) { $('a', text: 'Edit') }
-		deleteButton(to: ListRolePage) { $('input', value: 'Delete') }
+		editButton { $('a', text: 'Edit') }
+		deleteButton { $('input', value: 'Delete') }
 		row { String text -> $('li.fieldcontain span.property-label', text: text).parent() }
 		value { String text -> row(text).find('span.property-value').text() }
 		authority { value('Authority') }
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/user/CreateUserPage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/user/CreateUserPage.groovy
index 136e5178a..ee7b0caf1 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/user/CreateUserPage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/user/CreateUserPage.groovy
@@ -19,10 +19,17 @@
 
 package pages.user
 
+import geb.module.Checkbox
+import geb.module.PasswordInput
+import geb.module.TextInput
 import pages.CreatePage
 
 class CreateUserPage extends CreatePage {
+
 	static content = {
-		createButton(to: ShowUserPage) { create() }
+		usernameField { $('input', name: 'username').module(TextInput) }
+		passwordField { $('input', name: 'password').module(PasswordInput) }
+		enabledCheckbox { $('input', id: 'enabled').module(Checkbox) }
+		createButton(to: ShowUserPage) { $('input', name: 'create') }
 	}
 }
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/user/EditUserPage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/user/EditUserPage.groovy
index 3fcd83ac7..54554f7ec 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/user/EditUserPage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/user/EditUserPage.groovy
@@ -16,14 +16,20 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package pages.user
 
+import geb.module.Checkbox
+import geb.module.PasswordInput
+import geb.module.TextInput
 import pages.EditPage
 
 class EditUserPage extends EditPage {
+
 	static content = {
-		updateButton(to: ShowUserPage) { $('input', value: 'Update') }
-		deleteButton(to: ListUserPage) { $('input', value: 'Delete') }
+		usernameField { $('input', name: 'username').module(TextInput) }
+		passwordField { $('input', name: 'password').module(PasswordInput) }
+		enabledCheckbox { $('input', id: 'enabled').module(Checkbox) }
+		updateButton { $('input', value: 'Update') }
+		deleteButton { $('input', value: 'Delete') }
 	}
 }
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/user/ListUserPage.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/user/ListUserPage.groovy
index 020d35cb0..3c78f9c26 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/user/ListUserPage.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/pages/user/ListUserPage.groovy
@@ -16,7 +16,6 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package pages.user
 
 import geb.Module
@@ -33,16 +32,16 @@ class ListUserPage extends ScaffoldPage {
 	static content = {
 		newUserButton(to: CreateUserPage) { $('a', text: 'New TestUser') }
 		userTable { $('div.list table', 0) }
-		userRow { i -> userRows[i].module UserRow }
+		userRow { int i -> userRows[i].module(UserRow) }
 		userRows(required: false) { userTable.find('tbody').find('tr') }
 	}
 }
 
 class UserRow extends Module {
 	static content = {
-		cell { i -> $('td', i) }
-		cellText { i -> cell(i).text() }
-		cellHrefText{ i -> cell(i).find('a').text() }
+		cell { int i -> $('td', i) }
+		cellText { int i -> cell(i).text() }
+		cellHrefText{ int i -> cell(i).find('a').text() }
 		username { cellText(1) }
 		userEnabled { 'True' == cellText(2) }
 		showLink(to: ShowUserPage) { cell(0).find('a') }
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/InheritanceSecuritySpec.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/InheritanceSecuritySpec.groovy
index 5d0898a5a..72e66346f 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/InheritanceSecuritySpec.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/InheritanceSecuritySpec.groovy
@@ -19,6 +19,8 @@
 
 package specs
 
+import spock.lang.Unroll
+
 import grails.testing.mixin.integration.Integration
 import pages.IndexPage
 import pages.LoginPage
@@ -33,6 +35,7 @@ class InheritanceSecuritySpec extends AbstractSecuritySpec {
 		go 'testData/addTestUsers'
 	}
 
+	@Unroll
 	void 'should redirect to login page for anonymous'() {
 		when:
 		go uri
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/RequestmapSpec.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/RequestmapSpec.groovy
index 01b19f066..e676397ae 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/RequestmapSpec.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/RequestmapSpec.groovy
@@ -20,6 +20,8 @@
 package specs
 
 import com.testapp.TestDataService
+import spock.lang.Stepwise
+
 import grails.testing.mixin.integration.Integration
 import pages.requestmap.CreateRequestmapPage
 import pages.requestmap.EditRequestmapPage
@@ -27,100 +29,82 @@ import pages.requestmap.ListRequestmapPage
 import pages.requestmap.ShowRequestmapPage
 import spock.lang.IgnoreIf
 
+@Stepwise
 @Integration
 @IgnoreIf({ System.getProperty('TESTCONFIG') != 'requestmap' })
 class RequestmapSpec extends AbstractSecuritySpec {
 
 	void 'test request maps are initially present'() {
 		when:
-		go 'testRequestmap/list?max=100'
+		def page = to(ListRequestmapPage, max: 100)
 
 		then:
-		at ListRequestmapPage
-		requestmapRows.size() == TestDataService.URIS_FOR_REQUESTMAPS.size()
+		page.requestmapRows.size() == TestDataService.URIS_FOR_REQUESTMAPS.size()
 	}
 
 	void 'add a requestmap'() {
 		when:
-		to ListRequestmapPage
-		newRequestmapButton.click()
-
-		then:
-		at CreateRequestmapPage
+		to(ListRequestmapPage).with {
+			newRequestmapButton.click()
+		}
 
-		when:
-		$('form').url = '/nuevo/**'
-		configAttribute = 'ROLE_ADMIN'
-		createButton.click()
+		and:
+		def page = at(CreateRequestmapPage)
+		page.urlField.text = '/nuevo/**'
+		page.configAttributeField.text = 'ROLE_ADMIN'
+		page.createButton.click()
+		page = at(ShowRequestmapPage)
 
 		then:
-		at ShowRequestmapPage
-		value('URL') == '/nuevo/**'
-		configAttribute == 'ROLE_ADMIN'
+		page.value('URL') == '/nuevo/**'
+		page.configAttribute == 'ROLE_ADMIN'
 
 		when:
-		go 'testRequestmap/list?max=100'
+		page = to(ListRequestmapPage, max: 100)
 
 		then:
-		at ListRequestmapPage
-		requestmapRows.size() == (TestDataService.URIS_FOR_REQUESTMAPS.size() + 1)
+		page.requestmapRows.size() == (TestDataService.URIS_FOR_REQUESTMAPS.size() + 1)
 	}
 
 	void 'edit the details'() {
 		when:
-		go 'testRequestmap/list?max=100'
+		def page = to(ListRequestmapPage, max: 100)
+		page.requestmapRow(19).showLink.click()
+		page = at(ShowRequestmapPage)
 
-		then:
-		at ListRequestmapPage
+		and:
+		page.editButton.click()
+		page = at(EditRequestmapPage)
 
-		when:
-		requestmapRow(19).showLink.click()
+		and:
+		page.urlField.text = '/secure2/**'
+		page.configAttributeField.text = 'ROLE_ADMINX'
+		page.updateButton.click()
+		page = at(ShowRequestmapPage)
 
 		then:
-		at ShowRequestmapPage
-
-		when:
-		editButton.click()
-
-		then:
-		at EditRequestmapPage
-
-		when:
-		$('form').url = '/secure2/**'
-		configAttribute = 'ROLE_ADMINX'
-		updateButton.click()
-
-		then:
-		at ShowRequestmapPage
-		value('URL') == '/secure2/**'
-		configAttribute == 'ROLE_ADMINX'
+		page.value('URL') == '/secure2/**'
+		page.configAttribute == 'ROLE_ADMINX'
 	}
 
 	void 'delete requestmap'() {
 		when:
-		go 'testRequestmap/list?max=100'
-
-		then:
-		at ListRequestmapPage
-
-		when:
-		requestmapRow(19).showLink.click()
+		def page = to(ListRequestmapPage, max: 100)
+		page.requestmapRow(19).showLink.click()
+		page = at(ShowRequestmapPage)
+		def deletedId = page.id
 
-		then:
-		at ShowRequestmapPage
-
-		when:
-		def deletedId = id
-		withConfirm { deleteButton.click() }
+		and:
+		withConfirm { page.deleteButton.click() }
+		page = at(ListRequestmapPage)
 
 		then:
-		at ListRequestmapPage
-		message == "TestRequestmap $deletedId deleted"
+		page.message == "TestRequestmap $deletedId deleted"
 
 		when:
-		go 'testRequestmap/list?max=100'
+		page = to(ListRequestmapPage, max: 100)
 
 		then:
-		requestmapRows.size() == TestDataService.URIS_FOR_REQUESTMAPS.size()
+		page.requestmapRows.size() == TestDataService.URIS_FOR_REQUESTMAPS.size()
 	}
 }
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/RoleSpec.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/RoleSpec.groovy
index 96ffcdcc4..0074a4171 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/RoleSpec.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/RoleSpec.groovy
@@ -16,16 +16,18 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package specs
 
-import grails.testing.mixin.integration.Integration
 import pages.role.CreateRolePage
 import pages.role.EditRolePage
 import pages.role.ListRolePage
 import pages.role.ShowRolePage
 import spock.lang.IgnoreIf
+import spock.lang.Stepwise
 
+import grails.testing.mixin.integration.Integration
+
+@Stepwise
 @Integration
 @IgnoreIf({ !(
 		System.getProperty('TESTCONFIG') == 'annotation' ||
@@ -38,85 +40,81 @@ class RoleSpec extends AbstractSecuritySpec {
 
 	void 'there are no roles initially'() {
 		when:
-		to ListRolePage
+		def page = to(ListRolePage)
 
 		then:
-		roleRows.size() == 0
+		page.roleRows.size() == 0
 	}
 
 	void 'add a role'() {
 		when:
-		to ListRolePage
-		newRoleButton.click()
+		def page = to(ListRolePage)
+		page.newRoleButton.click()
 
-		then:
-		at CreateRolePage
+		and:
+		page = at(CreateRolePage)
+		page.authorityField.text = 'test'
+		page.createButton.click()
 
-		when:
-		authority = 'test'
-		createButton.click()
+		and:
+		page = at(ShowRolePage)
 
 		then:
-		at ShowRolePage
-		authority == 'test'
+		page.authority == 'test'
 	}
 
 	void 'edit the details'() {
 		when:
-		to ListRolePage
-		roleRow(0).showLink.click()
+		def page = to(ListRolePage)
+		page.roleRow(0).showLink.click()
+		page = at(ShowRolePage)
 
-		then:
-		at ShowRolePage
+		and:
+		page.editButton.click()
+		page = at(EditRolePage)
 
-		when:
-		editButton.click()
+		and:
+		page.authorityField.text = 'test_new'
+		page.updateButton.click()
 
 		then:
-		at EditRolePage
+		at(ShowRolePage)
 
 		when:
-		authority = 'test_new'
-		updateButton.click()
+		page = to(ListRolePage)
 
 		then:
-		at ShowRolePage
-
-		when:
-		to ListRolePage
-
-		then:
-		roleRows.size() == 1
-
-		def row = roleRow(0)
-		row.authority == 'test_new'
+		//page.roleRows.size() == 1
+		page.roleRow(0).authority == 'test_new'
 	}
 
 	void 'show role'() {
 		when:
-		to ListRolePage
-		roleRow(0).showLink.click()
+		def page = to(ListRolePage)
+		page.roleRow(0).showLink.click()
 
 		then:
-		at ShowRolePage
+		at(ShowRolePage)
 	}
 
 	void 'delete role'() {
 		when:
-		to ListRolePage
-		roleRow(0).showLink.click()
-		def deletedId = id
+		def page = to(ListRolePage).tap {
+			roleRow(0).showLink.click()
+		}
+		def deletedId = page.id
 
-		then:
-		at ShowRolePage
+		and:
+		page = at(ShowRolePage)
 
-		when:
-		withConfirm { deleteButton.click() }
+		and:
+		withConfirm { page.deleteButton.click() }
 
-		then:
-		at ListRolePage
+		and:
+		page = at(ListRolePage)
 
-		message == "TestRole $deletedId deleted"
-		roleRows.size() == 0
+		then:
+		page.message == "TestRole $deletedId deleted"
+		page.roleRows.size() == 0
 	}
 }
diff --git a/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/UserSpec.groovy b/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/UserSpec.groovy
index 30b2c499d..eafb922f9 100644
--- a/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/UserSpec.groovy
+++ b/plugin-core/examples/functional-test-app/src/integration-test/groovy/specs/UserSpec.groovy
@@ -16,114 +16,105 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package specs
 
-import grails.testing.mixin.integration.Integration
 import pages.user.CreateUserPage
 import pages.user.EditUserPage
 import pages.user.ListUserPage
 import pages.user.ShowUserPage
 import spock.lang.IgnoreIf
+import spock.lang.Stepwise
 
+import grails.testing.mixin.integration.Integration
+
+@Stepwise
 @Integration
 @IgnoreIf({ !(
 		System.getProperty('TESTCONFIG') == 'annotation' ||
-				System.getProperty('TESTCONFIG') == 'basic' ||
-				System.getProperty('TESTCONFIG') == 'basicCacheUsers' ||
-				System.getProperty('TESTCONFIG') == 'requestmap' ||
-				System.getProperty('TESTCONFIG') == 'static')
+		System.getProperty('TESTCONFIG') == 'basic' ||
+		System.getProperty('TESTCONFIG') == 'basicCacheUsers' ||
+		System.getProperty('TESTCONFIG') == 'requestmap' ||
+		System.getProperty('TESTCONFIG') == 'static')
 })
 class UserSpec extends AbstractSecuritySpec {
 
 	void 'there are no users initially'() {
 		when:
-		to ListUserPage
+		def page = to(ListUserPage)
 
 		then:
-		userRows.size() == 0
+		page.userRows.size() == 0
 	}
 
 	void 'add a user'() {
 		when:
-		to ListUserPage
-		newUserButton.click()
-
-		then:
-		at CreateUserPage
+		def page = to(ListUserPage)
+		page.newUserButton.click()
 
-		when:
-		username = 'new_user'
-		password = 'p4ssw0rd'
-		$('#enabled').click()
-		createButton.click()
+		and:
+		page = at(CreateUserPage)
+		page.usernameField.text = 'new_user'
+		page.passwordField.text = 'p4ssw0rd'
+		page.enabledCheckbox.check()
+		page.createButton.click()
+		page = at(ShowUserPage)
 
 		then:
-		at ShowUserPage
-		username == 'new_user'
-		userEnabled == true
+		page.username == 'new_user'
+		page.userEnabled == true
 	}
 
 	void 'edit the details'() {
 		when:
-		to ListUserPage
-		userRow(0).showLink.click()
+		def page = to(ListUserPage)
+		page.userRow(0).showLink.click()
 
-		then:
-		at ShowUserPage
+		and:
+		page = at(ShowUserPage)
+		page.editButton.click()
+		page = at(EditUserPage)
 
-		when:
-		editButton.click()
+		and:
+		page.usernameField.text = 'new_user2'
+		page.passwordField.text = 'p4ssw0rd2'
+		page.enabledCheckbox.uncheck()
+		page.updateButton.click()
 
 		then:
-		at EditUserPage
+		at(ShowUserPage)
 
 		when:
-		username = 'new_user2'
-		password = 'p4ssw0rd2'
-		$('#enabled').click()
-
-		updateButton.click()
+		page = to(ListUserPage)
 
 		then:
-		at ShowUserPage
-
-		when:
-		to ListUserPage
-
-		then:
-		userRows.size() == 1
-
-		def row = userRow(0)
+		page.userRows.size() == 1
+		def row = page.userRow(0)
 		row.username == 'new_user2'
 		!row.userEnabled
 	}
 
 	void 'show user'() {
 		when:
-		to ListUserPage
-		userRow(0).showLink.click()
+		def page = to(ListUserPage)
+		page.userRow(0).showLink.click()
 
 		then:
-		at ShowUserPage
+		at(ShowUserPage)
 	}
 
 	void 'delete user'() {
 		when:
-		to ListUserPage
-		userRow(0).showLink.click()
-		def deletedId = id
+		def page = to(ListUserPage)
+		page.userRow(0).showLink.click()
+		def deletedId = page.id
+		page = at(ShowUserPage)
 
-		then:
-		at ShowUserPage
-
-		when:
-		withConfirm { deleteButton.click() }
+		and:
+		withConfirm { page.deleteButton.click() }
+		page = at(ListUserPage)
 
 		then:
-		at ListUserPage
-
-		message == "TestUser $deletedId deleted."
-		userRows.size() == 0
+		page.message == "TestUser $deletedId deleted."
+		page.userRows.size() == 0
 	}
 }
diff --git a/plugin-core/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluderIntegrationSpec.groovy b/plugin-core/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluderIntegrationSpec.groovy
index 22a0ad2ca..c4fbc68d6 100644
--- a/plugin-core/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluderIntegrationSpec.groovy
+++ b/plugin-core/examples/integration-test-app/src/integration-test/groovy/grails/plugin/springsecurity/SecurityAutoConfigurationExcluderIntegrationSpec.groovy
@@ -40,28 +40,6 @@ class SecurityAutoConfigurationExcluderIntegrationSpec extends Specification {
         )
     }
 
-    void "SecurityAutoConfiguration bean is not registered"() {
-        given:
-        def secAutoConfig = Class.forName(
-                'org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration'
-        )
-
-        expect:
-        applicationContext
-                .getBeanNamesForType(secAutoConfig).length == 0
-    }
-
-    void "SecurityFilterAutoConfiguration bean is not registered"() {
-        given:
-        def secFilterAutoConfig = Class.forName(
-                'org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration'
-        )
-
-        expect:
-        applicationContext
-                .getBeanNamesForType(secFilterAutoConfig).length == 0
-    }
-
     void "no duplicate SecurityFilterChain beans from auto-configuration"() {
         given:
         def filterChainBeans = applicationContext
diff --git a/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/SecurityEventListenerSpec.groovy b/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/SecurityEventListenerSpec.groovy
index e24bb3f83..1e5c4f328 100644
--- a/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/SecurityEventListenerSpec.groovy
+++ b/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/SecurityEventListenerSpec.groovy
@@ -18,7 +18,8 @@
  */
 package grails.plugin.springsecurity
 
-import org.springframework.security.access.event.AbstractAuthorizationEvent
+import org.springframework.security.authorization.AuthorizationDecision
+import org.springframework.security.authorization.event.AuthorizationEvent
 import org.springframework.security.authentication.BadCredentialsException
 import org.springframework.security.authentication.TestingAuthenticationToken
 import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent
@@ -77,12 +78,12 @@ class SecurityEventListenerSpec extends AbstractUnitSpec {
 		called
 	}
 
-	void 'Test handling AbstractAuthorizationEvent'() {
+	void 'Test handling AuthorizationEvent'() {
 		when:
 		boolean called = false
 		closures.onAuthorizationEvent = { e, appCtx -> called = true }
 
-		listener.onApplicationEvent new AbstractAuthorizationEvent(42) {}
+		listener.onApplicationEvent new AuthorizationEvent({ new TestingAuthenticationToken('', '') }, 42, new AuthorizationDecision(true))
 
 		then:
 		called
diff --git a/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/SpringSecurityUtilsSpec.groovy b/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/SpringSecurityUtilsSpec.groovy
index 6348432d6..401edd314 100644
--- a/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/SpringSecurityUtilsSpec.groovy
+++ b/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/SpringSecurityUtilsSpec.groovy
@@ -218,7 +218,7 @@ class SpringSecurityUtilsSpec extends AbstractUnitSpec {
 
 	void 'isAjax using SavedRequest, false'() {
 		when:
-		def savedRequest = new DefaultSavedRequest(request, new PortResolverImpl())
+		def savedRequest = new DefaultSavedRequest(request)
 		request.session.setAttribute SpringSecurityUtils.SAVED_REQUEST, savedRequest
 
 		then:
@@ -228,7 +228,7 @@ class SpringSecurityUtilsSpec extends AbstractUnitSpec {
 	void 'isAjax using SavedRequest, true'() {
 		when:
 		request.addHeader 'X-Requested-With', 'true'
-		def savedRequest = new DefaultSavedRequest(request, new PortResolverImpl())
+		def savedRequest = new DefaultSavedRequest(request)
 		request.session.setAttribute SpringSecurityUtils.SAVED_REQUEST, savedRequest
 
 		then:
@@ -238,7 +238,7 @@ class SpringSecurityUtilsSpec extends AbstractUnitSpec {
 	void 'isAjax using SavedRequest, XMLHttpRequest'() {
 		when:
 		request.addHeader 'X-Requested-With', 'XMLHttpRequest'
-		def savedRequest = new DefaultSavedRequest(request, new PortResolverImpl())
+		def savedRequest = new DefaultSavedRequest(request)
 		request.session.setAttribute SpringSecurityUtils.SAVED_REQUEST, savedRequest
 
 		then:
@@ -431,7 +431,7 @@ class SpringSecurityUtilsSpec extends AbstractUnitSpec {
 
 	private void initRoleHierarchy(String hierarchyString) {
 		defineBeans {
-			roleHierarchy(RoleHierarchyImpl) {
+			roleHierarchy(MutableRoleHierarchy) {
 				hierarchy = hierarchyString
 			}
 		}
diff --git a/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/web/access/intercept/FilterSecurityInterceptorSpec.groovy b/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/web/access/intercept/FilterSecurityInterceptorSpec.groovy
new file mode 100644
index 000000000..b21adf64a
--- /dev/null
+++ b/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/web/access/intercept/FilterSecurityInterceptorSpec.groovy
@@ -0,0 +1,71 @@
+/*
+ *  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 grails.plugin.springsecurity.web.access.intercept
+
+import grails.plugin.springsecurity.AbstractUnitSpec
+import org.springframework.security.access.AccessDecisionManager
+import org.springframework.security.access.SecurityConfig
+import org.springframework.security.web.FilterInvocation
+import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource
+import org.springframework.security.web.access.intercept.FilterSecurityInterceptor
+
+import jakarta.servlet.FilterChain
+
+class FilterSecurityInterceptorSpec extends AbstractUnitSpec {
+
+	void 'doFilter delegates to the chain for public invocations when allowed'() {
+		given:
+		def interceptor = new FilterSecurityInterceptor(
+			rejectPublicInvocations: false,
+			securityMetadataSource: Stub(FilterInvocationSecurityMetadataSource) {
+				getAttributes(_ as FilterInvocation) >> null
+			}
+		)
+		FilterChain chain = Mock()
+
+		when:
+		interceptor.doFilter(request, response, chain)
+
+		then:
+		1 * chain.doFilter(request, response)
+	}
+
+	void 'doFilter consults the access decision manager before continuing the chain'() {
+		given:
+		def attributes = [new SecurityConfig('ROLE_USER')]
+		def interceptor = new FilterSecurityInterceptor(
+			rejectPublicInvocations: false,
+			securityMetadataSource: Stub(FilterInvocationSecurityMetadataSource) {
+				getAttributes(_ as FilterInvocation) >> attributes
+			}
+		)
+		interceptor.accessDecisionManager = Mock(AccessDecisionManager) {
+			1 * decide(null, _ as FilterInvocation, attributes)
+		}
+		FilterChain chain = Mock()
+
+		when:
+		interceptor.doFilter(request, response, chain)
+
+		then:
+		1 * chain.doFilter(request, response)
+	}
+}
+
+
diff --git a/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/web/authentication/AjaxAwareAuthenticationEntryPointSpec.groovy b/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/web/authentication/AjaxAwareAuthenticationEntryPointSpec.groovy
index f55c13b1c..db8294e9b 100644
--- a/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/web/authentication/AjaxAwareAuthenticationEntryPointSpec.groovy
+++ b/plugin-core/plugin/src/test/groovy/grails/plugin/springsecurity/web/authentication/AjaxAwareAuthenticationEntryPointSpec.groovy
@@ -22,6 +22,7 @@ import grails.plugin.springsecurity.AbstractUnitSpec
 import grails.plugin.springsecurity.ReflectionUtils
 import grails.plugin.springsecurity.SpringSecurityUtils
 import grails.plugin.springsecurity.web.SecurityRequestHolder
+import org.springframework.security.web.RedirectStrategy
 
 /**
  * Unit tests for WithAjaxAuthenticationProcessingFilterEntryPoint.
@@ -60,6 +61,23 @@ class AjaxAwareAuthenticationEntryPointSpec extends AbstractUnitSpec {
 		ajaxLoginFormUrl == response.forwardedUrl
 	}
 
+	void 'commence() redirects without legacy portResolver wiring'() {
+		given:
+		entryPoint.useForward = false
+		String redirectedUrl
+		entryPoint.redirectStrategy = Stub(RedirectStrategy) {
+			sendRedirect(_, _, _) >> { requestArg, responseArg, url ->
+				redirectedUrl = url as String
+			}
+		}
+
+		when:
+		entryPoint.commence request, response, null
+
+		then:
+		loginFormUrl == redirectedUrl
+	}
+
 	void 'setAjaxLoginFormUrl'() {
 		when:
 		entryPoint.ajaxLoginFormUrl = 'foo'
diff --git a/plugin-core/plugin/src/test/groovy/org/springframework/security/access/intercept/aopalliance/MethodSecurityInterceptorSpec.groovy b/plugin-core/plugin/src/test/groovy/org/springframework/security/access/intercept/aopalliance/MethodSecurityInterceptorSpec.groovy
new file mode 100644
index 000000000..67892e0c2
--- /dev/null
+++ b/plugin-core/plugin/src/test/groovy/org/springframework/security/access/intercept/aopalliance/MethodSecurityInterceptorSpec.groovy
@@ -0,0 +1,147 @@
+/*
+ *  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.springframework.security.access.intercept.aopalliance
+
+import grails.plugin.springsecurity.SecurityTestUtils
+import org.aopalliance.aop.Advice
+import org.aopalliance.intercept.MethodInvocation
+import org.springframework.security.access.AccessDecisionManager
+import org.springframework.security.access.SecurityConfig
+import org.springframework.security.access.intercept.AfterInvocationManager
+import org.springframework.security.access.intercept.RunAsManagerImpl
+import org.springframework.security.access.method.MethodSecurityMetadataSource
+import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException
+import org.springframework.security.core.Authentication
+import org.springframework.security.core.context.SecurityContextHolder
+import spock.lang.Specification
+
+class MethodSecurityInterceptorSpec extends Specification {
+
+	void cleanup() {
+		SecurityTestUtils.logout()
+	}
+
+	void 'invoke acts as advice and applies access and after-invocation processing'() {
+		given:
+		Authentication authentication = SecurityTestUtils.authenticate(['ROLE_USER'])
+		def method = String.getMethod('trim')
+		def invocation = Stub(MethodInvocation) {
+			getMethod() >> method
+			getThis() >> '  ok  '
+			proceed() >> 'ok'
+		}
+		def attributes = [new SecurityConfig('ROLE_USER')]
+		def interceptor = new MethodSecurityInterceptor(
+			securityMetadataSource: Stub(MethodSecurityMetadataSource) {
+				getAttributes(method, String) >> attributes
+			},
+			accessDecisionManager: Mock(AccessDecisionManager) {
+				1 * decide(authentication, invocation, attributes)
+			},
+			afterInvocationManager: Mock(AfterInvocationManager) {
+				1 * decide(authentication, invocation, attributes, 'ok') >> 'filtered'
+			}
+		)
+
+		expect:
+		interceptor instanceof Advice
+		interceptor.invoke(invocation) == 'filtered'
+	}
+
+	void 'invoke proceeds directly when there are no security attributes'() {
+		given:
+		def method = String.getMethod('trim')
+		def invocation = Stub(MethodInvocation) {
+			getMethod() >> method
+			getThis() >> '  ok  '
+			proceed() >> 'ok'
+		}
+		def interceptor = new MethodSecurityInterceptor(
+			securityMetadataSource: Stub(MethodSecurityMetadataSource) {
+				getAttributes(method, String) >> null
+			}
+		)
+
+		expect:
+		interceptor.invoke(invocation) == 'ok'
+	}
+
+	void 'invoke fails with credentials not found when secured invocation has no authentication'() {
+		given:
+		def method = String.getMethod('trim')
+		def invocation = Stub(MethodInvocation) {
+			getMethod() >> method
+			getThis() >> '  ok  '
+		}
+		def interceptor = new MethodSecurityInterceptor(
+			securityMetadataSource: Stub(MethodSecurityMetadataSource) {
+				getAttributes(method, String) >> [new SecurityConfig('ROLE_USER')]
+			}
+		)
+
+		when:
+		interceptor.invoke(invocation)
+
+		then:
+		thrown AuthenticationCredentialsNotFoundException
+	}
+
+	void 'invoke applies run-as authentication during the secured invocation and restores the original authentication afterwards'() {
+		given:
+		Authentication authentication = SecurityTestUtils.authenticate(['ROLE_ADMIN'])
+		def method = String.getMethod('trim')
+		def attributes = [new SecurityConfig('ROLE_ADMIN'), new SecurityConfig('RUN_AS_SUPERUSER')]
+		def interceptor = new MethodSecurityInterceptor(
+			securityMetadataSource: Stub(MethodSecurityMetadataSource) {
+				getAttributes(method, String) >> attributes
+			},
+			accessDecisionManager: Mock(AccessDecisionManager) {
+				1 * decide(authentication, _ as MethodInvocation, attributes)
+			},
+			runAsManager: new RunAsManagerImpl(),
+			afterInvocationManager: Mock(AfterInvocationManager) {
+				1 * decide(_ as Authentication, _ as MethodInvocation, attributes, 'ok') >> { Authentication activeAuthentication, MethodInvocation ignored, Collection ignoredAttributes, Object returnedObject ->
+					assert activeAuthentication.authorities*.authority.contains('ROLE_RUN_AS_SUPERUSER')
+					return returnedObject
+				}
+			}
+		)
+		def invocation = Stub(MethodInvocation) {
+			getMethod() >> method
+			getThis() >> '  ok  '
+			proceed() >> {
+				assert SecurityContextHolder.context.authentication.authorities*.authority.contains('ROLE_RUN_AS_SUPERUSER')
+				'ok'
+			}
+		}
+
+		when:
+		def result = interceptor.invoke(invocation)
+
+		then:
+		result == 'ok'
+		SecurityContextHolder.context.authentication.is(authentication)
+		!SecurityContextHolder.context.authentication.authorities*.authority.contains('ROLE_RUN_AS_SUPERUSER')
+	}
+}
+
+
+
+
+
diff --git a/plugin-core/plugin/src/test/groovy/org/springframework/security/access/method/AbstractFallbackMethodSecurityMetadataSourceSpec.groovy b/plugin-core/plugin/src/test/groovy/org/springframework/security/access/method/AbstractFallbackMethodSecurityMetadataSourceSpec.groovy
new file mode 100644
index 000000000..5b96cf886
--- /dev/null
+++ b/plugin-core/plugin/src/test/groovy/org/springframework/security/access/method/AbstractFallbackMethodSecurityMetadataSourceSpec.groovy
@@ -0,0 +1,60 @@
+/*
+ *  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.springframework.security.access.method
+
+import org.springframework.security.access.annotation.Secured
+import org.springframework.security.access.annotation.SecuredAnnotationSecurityMetadataSource
+import spock.lang.Specification
+
+class AbstractFallbackMethodSecurityMetadataSourceSpec extends Specification {
+
+	private final SecuredAnnotationSecurityMetadataSource metadataSource = new SecuredAnnotationSecurityMetadataSource()
+
+	void 'getAttributes prefers method metadata from the concrete target class over class metadata'() {
+		given:
+		def method = SecuredService.getMethod('userAnnotated')
+
+		expect:
+		metadataSource.getAttributes(method, SecuredServiceImpl)*.attribute == ['ROLE_USER']
+	}
+
+	void 'getAttributes falls back to class metadata when the concrete target method has no annotation'() {
+		given:
+		def method = SecuredService.getMethod('notAnnotated')
+
+		expect:
+		metadataSource.getAttributes(method, SecuredServiceImpl)*.attribute == ['ROLE_ADMIN']
+	}
+
+	private static interface SecuredService {
+		void notAnnotated()
+		void userAnnotated()
+	}
+
+	@Secured('ROLE_ADMIN')
+	private static class SecuredServiceImpl implements SecuredService {
+		@Override
+		void notAnnotated() {}
+
+		@Override
+		@Secured('ROLE_USER')
+		void userAnnotated() {}
+	}
+}
+
diff --git a/plugin-core/plugin/src/test/groovy/org/springframework/security/access/prepost/MethodSecurityExpressionCompatibilitySpec.groovy b/plugin-core/plugin/src/test/groovy/org/springframework/security/access/prepost/MethodSecurityExpressionCompatibilitySpec.groovy
new file mode 100644
index 000000000..954d129d0
--- /dev/null
+++ b/plugin-core/plugin/src/test/groovy/org/springframework/security/access/prepost/MethodSecurityExpressionCompatibilitySpec.groovy
@@ -0,0 +1,155 @@
+/*
+ *  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.springframework.security.access.prepost
+
+import grails.plugin.springsecurity.SecurityTestUtils
+import org.aopalliance.intercept.MethodInvocation
+import org.springframework.security.access.PermissionEvaluator
+import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler
+import org.springframework.security.access.expression.method.ExpressionBasedAnnotationAttributeFactory
+import org.springframework.security.access.expression.method.ExpressionBasedPostInvocationAdvice
+import org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice
+import org.springframework.security.core.Authentication
+import org.springframework.security.core.parameters.P
+import spock.lang.Specification
+
+class MethodSecurityExpressionCompatibilitySpec extends Specification {
+
+	void cleanup() {
+		SecurityTestUtils.logout()
+	}
+
+	void 'metadata source extracts pre and post annotations from secured methods'() {
+		given:
+		def metadataSource = new PrePostAnnotationSecurityMetadataSource(
+				new ExpressionBasedAnnotationAttributeFactory(Stub(DefaultMethodSecurityExpressionHandler))
+		)
+		def method = SecuredService.getMethod('getReports')
+
+		when:
+		def attribute = metadataSource.getAttributes(method, SecuredService).first() as ExpressionBasedAnnotationConfigAttribute
+
+		then:
+		attribute.preAuthorizeExpression == 'hasRole("ROLE_USER")'
+		attribute.postFilterExpression == 'hasPermission(filterObject, read)'
+	}
+
+	void 'pre-invocation voter denies and grants based on the evaluated pre-authorize expression'() {
+		given:
+		PermissionEvaluator permissionEvaluator = Mock()
+		Authentication authentication = SecurityTestUtils.authenticate(['ROLE_USER'])
+		def advice = new ExpressionBasedPreInvocationAdvice(
+				expressionHandler: Stub(DefaultMethodSecurityExpressionHandler) {
+					getPermissionEvaluator() >> permissionEvaluator
+				}
+		)
+		def voter = new PreInvocationAuthorizationAdviceVoter(advice)
+		def reflectedMethod = SecuredService.getMethod('getReport', Long)
+		MethodInvocation invocation = Stub(MethodInvocation)
+		invocation.getMethod() >> reflectedMethod
+		invocation.getArguments() >> ([42L] as Object[])
+		def attribute = new ExpressionBasedAnnotationConfigAttribute(
+				'hasPermission(#id, "com.testacl.Report", read)', null, null, null)
+
+		when:
+		def denied = voter.vote(authentication, invocation, [attribute])
+
+		then:
+		denied == PreInvocationAuthorizationAdviceVoter.ACCESS_DENIED
+		1 * permissionEvaluator.hasPermission(authentication, 42L, 'com.testacl.Report', _) >> false
+
+		when:
+		def granted = voter.vote(authentication, invocation, [attribute])
+
+		then:
+		granted == PreInvocationAuthorizationAdviceVoter.ACCESS_GRANTED
+		1 * permissionEvaluator.hasPermission(authentication, 42L, 'com.testacl.Report', _) >> true
+	}
+
+	void 'pre-invocation voter exposes the create ACL permission alias'() {
+		given:
+		PermissionEvaluator permissionEvaluator = Mock()
+		Authentication authentication = SecurityTestUtils.authenticate(['ROLE_USER'])
+		def advice = new ExpressionBasedPreInvocationAdvice(
+				expressionHandler: Stub(DefaultMethodSecurityExpressionHandler) {
+					getPermissionEvaluator() >> permissionEvaluator
+				}
+		)
+		def voter = new PreInvocationAuthorizationAdviceVoter(advice)
+		def reflectedMethod = SecuredService.getMethod('createReport', Long)
+		MethodInvocation invocation = Stub(MethodInvocation)
+		invocation.getMethod() >> reflectedMethod
+		invocation.getArguments() >> ([42L] as Object[])
+		def attribute = new ExpressionBasedAnnotationConfigAttribute(
+				'hasPermission(#id, "com.testacl.Report", create)', null, null, null)
+
+		when:
+		def granted = voter.vote(authentication, invocation, [attribute])
+
+		then:
+		granted == PreInvocationAuthorizationAdviceVoter.ACCESS_GRANTED
+		1 * permissionEvaluator.hasPermission(authentication, 42L, 'com.testacl.Report', 4) >> true
+	}
+
+	void 'post-invocation advice filters returned collections using post-filter expressions'() {
+		given:
+		PermissionEvaluator permissionEvaluator = Mock()
+		Authentication authentication = SecurityTestUtils.authenticate(['ROLE_USER'])
+		def advice = new ExpressionBasedPostInvocationAdvice(Stub(DefaultMethodSecurityExpressionHandler) {
+			getPermissionEvaluator() >> permissionEvaluator
+		})
+		def reflectedMethod = SecuredService.getMethod('getReports')
+		MethodInvocation invocation = Stub(MethodInvocation)
+		invocation.getMethod() >> reflectedMethod
+		invocation.getArguments() >> ([] as Object[])
+		def attribute = new ExpressionBasedAnnotationConfigAttribute(
+				null, null, null, 'hasPermission(filterObject, read)')
+		def report1 = new Object()
+		def report2 = new Object()
+
+		when:
+		def filtered = advice.after(authentication, invocation, attribute, [report1, report2])
+
+		then:
+		filtered == [report1]
+		1 * permissionEvaluator.hasPermission(authentication, report1, _) >> true
+		1 * permissionEvaluator.hasPermission(authentication, report2, _) >> false
+	}
+
+	private static class SecuredService {
+		@PreAuthorize('hasPermission(#id, "com.testacl.Report", read)')
+		Object getReport(@P('id') Long id) {
+			null
+		}
+
+		@PreAuthorize('hasPermission(#id, "com.testacl.Report", create)')
+		Object createReport(@P('id') Long id) {
+			null
+		}
+
+		@PreAuthorize('hasRole("ROLE_USER")')
+		@PostFilter('hasPermission(filterObject, read)')
+		List<Object> getReports() {
+			[]
+		}
+	}
+}
+
+
+
diff --git a/plugin-rest/spring-security-rest/src/test/groovy/grails/plugin/springsecurity/rest/authentication/DefaultRestAuthenticationEventPublisherSpec.groovy b/plugin-rest/spring-security-rest/src/test/groovy/grails/plugin/springsecurity/rest/authentication/DefaultRestAuthenticationEventPublisherSpec.groovy
index 259c9fe51..68c52e9b9 100644
--- a/plugin-rest/spring-security-rest/src/test/groovy/grails/plugin/springsecurity/rest/authentication/DefaultRestAuthenticationEventPublisherSpec.groovy
+++ b/plugin-rest/spring-security-rest/src/test/groovy/grails/plugin/springsecurity/rest/authentication/DefaultRestAuthenticationEventPublisherSpec.groovy
@@ -82,4 +82,22 @@ class DefaultRestAuthenticationEventPublisherSpec extends Specification {
         then:
         0 * eventPublisher.publishEvent(_)
     }
+
+    void "should allow null publisher in constructor and remain no-op for token events"() {
+        when:
+        new DefaultRestAuthenticationEventPublisher(null).publishTokenCreation(accessToken)
+
+        then:
+        noExceptionThrown()
+        0 * eventPublisher.publishEvent(_)
+    }
+
+    void "should not execute parent publishEvent when publisher is not set"() {
+        when:
+        new DefaultRestAuthenticationEventPublisher().publishAuthenticationSuccess(accessToken)
+
+        then:
+        noExceptionThrown()
+        0 * eventPublisher.publishEvent(_)
+    }
 }
diff --git a/plugin-ui/examples/extended/src/integration-test/groovy/page/AbstractSecurityPage.groovy b/plugin-ui/examples/extended/src/integration-test/groovy/page/AbstractSecurityPage.groovy
index 66151831c..2cbe93709 100644
--- a/plugin-ui/examples/extended/src/integration-test/groovy/page/AbstractSecurityPage.groovy
+++ b/plugin-ui/examples/extended/src/integration-test/groovy/page/AbstractSecurityPage.groovy
@@ -23,6 +23,10 @@ import geb.Page
 
 abstract class AbstractSecurityPage extends Page {
 
+	static content = {
+		submitBtn { $('input', type: 'submit') }
+	}
+
 	void submit() {
 		submitBtn.click()
 	}
diff --git a/plugin-ui/examples/extended/src/integration-test/groovy/spec/RegisterSpec.groovy b/plugin-ui/examples/extended/src/integration-test/groovy/spec/RegisterSpec.groovy
index 234a8637f..a73b88587 100644
--- a/plugin-ui/examples/extended/src/integration-test/groovy/spec/RegisterSpec.groovy
+++ b/plugin-ui/examples/extended/src/integration-test/groovy/spec/RegisterSpec.groovy
@@ -16,10 +16,8 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package spec
 
-import grails.testing.mixin.integration.Integration
 import page.profile.ProfileCreatePage
 import page.profile.ProfileEditPage
 import page.profile.ProfileListPage
@@ -30,6 +28,8 @@ import page.register.SecurityQuestionsPage
 import page.user.UserEditPage
 import page.user.UserSearchPage
 
+import grails.testing.mixin.integration.Integration
+
 @Integration
 class RegisterSpec extends AbstractSecuritySpec {
 
diff --git a/plugin-ui/examples/simple/src/integration-test/groovy/page/AbstractSecurityPage.groovy b/plugin-ui/examples/simple/src/integration-test/groovy/page/AbstractSecurityPage.groovy
index 66151831c..2cbe93709 100644
--- a/plugin-ui/examples/simple/src/integration-test/groovy/page/AbstractSecurityPage.groovy
+++ b/plugin-ui/examples/simple/src/integration-test/groovy/page/AbstractSecurityPage.groovy
@@ -23,6 +23,10 @@ import geb.Page
 
 abstract class AbstractSecurityPage extends Page {
 
+	static content = {
+		submitBtn { $('input', type: 'submit') }
+	}
+
 	void submit() {
 		submitBtn.click()
 	}
diff --git a/plugin-ui/examples/simple/src/integration-test/groovy/page/register/RegisterPage.groovy b/plugin-ui/examples/simple/src/integration-test/groovy/page/register/RegisterPage.groovy
index cb9ff69af..30eb8f00a 100644
--- a/plugin-ui/examples/simple/src/integration-test/groovy/page/register/RegisterPage.groovy
+++ b/plugin-ui/examples/simple/src/integration-test/groovy/page/register/RegisterPage.groovy
@@ -16,7 +16,6 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package page.register
 
 import geb.module.PasswordInput
diff --git a/plugin-ui/examples/simple/src/integration-test/groovy/spec/RegisterSpec.groovy b/plugin-ui/examples/simple/src/integration-test/groovy/spec/RegisterSpec.groovy
index 6da4b24df..1e34c404e 100644
--- a/plugin-ui/examples/simple/src/integration-test/groovy/spec/RegisterSpec.groovy
+++ b/plugin-ui/examples/simple/src/integration-test/groovy/spec/RegisterSpec.groovy
@@ -16,32 +16,23 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-
 package spec
 
-import grails.testing.mixin.integration.Integration
+import com.dumbster.smtp.SimpleSmtpServer
+import com.dumbster.smtp.SmtpMessage
 import page.register.ForgotPasswordPage
 import page.register.RegisterPage
 import page.register.ResetPasswordPage
 import page.user.UserEditPage
 import page.user.UserSearchPage
 
-import com.dumbster.smtp.SimpleSmtpServer
-import com.dumbster.smtp.SmtpMessage
+import grails.testing.mixin.integration.Integration
 
 @Integration
 class RegisterSpec extends AbstractSecuritySpec {
 
 	private SimpleSmtpServer server
 
-	void setup() {
-		startMailServer()
-	}
-
-	void cleanup() {
-		server.stop()
-	}
-
 	void testRegisterValidation() {
 		when:
 		to(RegisterPage).with {
@@ -105,14 +96,14 @@ class RegisterSpec extends AbstractSecuritySpec {
 
 	void testRegisterAndForgotPassword() {
 		given:
-		String un = "test_user_abcdef${System.currentTimeMillis()}"
-
-		when:
-		def registerPage = to(RegisterPage)
+		startMailServer()
 
 		and:
-		registerPage.with {
-			username = un
+		def un = "test_user_abcdef${System.currentTimeMillis()}"
+
+		when:
+		to(RegisterPage).with {
+ 			username = un
 			email = "$un@abcdef.com"
 			password = 'aaaaaa1#'
 			password2 = 'aaaaaa1#'
@@ -121,22 +112,17 @@ class RegisterSpec extends AbstractSecuritySpec {
 
 		then:
 		pageSource.contains('Your account registration email was sent - check your mail!')
-		1 == server.receivedEmailSize
-
-		when:
-		def email = currentEmail
-
-		then:
-		'New Account' == email.getHeaderValue('Subject')
+		server.receivedEmailSize == 1
 
 		when:
-		String body = email.body
+		def smtpMessage = currentEmail
 
 		then:
-		body.contains("Hi $un")
+		'New Account' == smtpMessage.getHeaderValue('Subject')
+		smtpMessage.body.contains("Hi $un")
 
 		when:
-		String code = findCode(body, 'verifyRegistration')
+		String code = findCode(smtpMessage.body, 'verifyRegistration')
 
 		then:
 		code ==~ /^[a-f0-9]{32}$/
@@ -162,22 +148,18 @@ class RegisterSpec extends AbstractSecuritySpec {
 
 		then:
 		pageSource.contains('Your password reset email was sent - check your mail!')
-		2 == server.receivedEmailSize
+		server.receivedEmailSize == 2
 
 		when:
-		email = currentEmail
+		smtpMessage = currentEmail
 
 		then:
-		'Password Reset' == email.getHeaderValue('Subject')
+		smtpMessage.getHeaderValue('Subject') == 'Password Reset'
 
-		when:
-		body = email.body
-
-		then:
-		body.contains("Hi $un")
+		smtpMessage.body.contains("Hi $un")
 
 		when:
-		code = findCode(body, 'resetPassword')
+		code = findCode(smtpMessage.body, 'resetPassword')
 		go('register/resetPassword?t=123')
 
 		then:
@@ -247,6 +229,10 @@ class RegisterSpec extends AbstractSecuritySpec {
 
 		then:
 		pageSource.contains('User not found')
+
+		cleanup:
+		server?.stop()
+		server = null
 	}
 
 	private SmtpMessage getCurrentEmail() {
@@ -258,11 +244,11 @@ class RegisterSpec extends AbstractSecuritySpec {
 		return email as SmtpMessage
 	}
 
-	private String findCode(String body, String action) {
+	private static String findCode(String body, String action) {
 		def matcher = body =~ /(?s).*$action\?t=(.+)".*/
-		assert matcher.hasGroup()
-		assert matcher.count == 1
-		matcher[0][1]
+		assert matcher.find()
+		assert matcher.groupCount() == 1
+		matcher.group(1)
 	}
 
 	private void startMailServer() {
@@ -279,6 +265,6 @@ class RegisterSpec extends AbstractSecuritySpec {
 		}
 		server = SimpleSmtpServer.start(port)
 		go("testData/updateMailSenderPort?port=$port")
-		pageSource.contains("OK: $port")
+		assert pageSource.contains("OK: $port")
 	}
 }