Skip to content

Commit a35c774

Browse files
committed
Add scaffold test coverage
1 parent 0b3a1f2 commit a35c774

File tree

2 files changed

+280
-0
lines changed

2 files changed

+280
-0
lines changed

grails-scaffolding/build.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ dependencies {
4141
api project(':grails-rest-transforms')
4242

4343
compileOnly 'jline:jline'
44+
45+
testImplementation 'org.spockframework:spock-core'
46+
testImplementation project(':grails-web-gsp')
47+
testImplementation project(':grails-core')
48+
testImplementation 'jakarta.servlet:jakarta.servlet-api'
49+
testImplementation 'org.springframework:spring-web'
50+
testImplementation 'net.bytebuddy:byte-buddy'
4451
}
4552

4653
apply {
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package grails.plugin.scaffolding
20+
21+
import grails.core.GrailsControllerClass
22+
import grails.plugin.scaffolding.annotation.Scaffold
23+
import org.grails.gsp.GroovyPagesTemplateEngine
24+
import org.grails.web.gsp.io.GrailsConventionGroovyPageLocator
25+
import org.grails.web.servlet.mvc.GrailsWebRequest
26+
import org.grails.web.servlet.view.GroovyPageView
27+
import org.springframework.core.io.Resource
28+
import org.springframework.web.context.request.RequestContextHolder
29+
import spock.lang.Specification
30+
31+
class ScaffoldingViewResolverSpec extends Specification {
32+
33+
static final String TEST_NAMESPACE = "admin"
34+
static final String TEST_VIEW_NAME = "/event/index"
35+
36+
ScaffoldingViewResolver resolver
37+
GrailsConventionGroovyPageLocator mockPageLocator
38+
GroovyPagesTemplateEngine mockTemplateEngine
39+
GrailsWebRequest mockWebRequest
40+
GrailsControllerClass mockControllerClass
41+
42+
def setup() {
43+
resolver = new ScaffoldingViewResolver()
44+
mockPageLocator = Mock(GrailsConventionGroovyPageLocator)
45+
mockTemplateEngine = Mock(GroovyPagesTemplateEngine)
46+
mockControllerClass = Mock(GrailsControllerClass)
47+
48+
// Create GrailsWebRequest with required constructor args
49+
def mockHttpRequest = Mock(jakarta.servlet.http.HttpServletRequest)
50+
def mockHttpResponse = Mock(jakarta.servlet.http.HttpServletResponse)
51+
def mockServletContext = Mock(jakarta.servlet.ServletContext)
52+
mockWebRequest = Stub(GrailsWebRequest, constructorArgs: [mockHttpRequest, mockHttpResponse, mockServletContext])
53+
54+
resolver.groovyPageLocator = mockPageLocator
55+
resolver.templateEngine = mockTemplateEngine
56+
57+
// Set up thread local
58+
RequestContextHolder.setRequestAttributes(mockWebRequest)
59+
}
60+
61+
def cleanup() {
62+
RequestContextHolder.resetRequestAttributes()
63+
resolver.clearCache()
64+
}
65+
66+
// Helper methods
67+
void setupScaffoldController(Class controllerClazz, Class scaffoldDomain = null) {
68+
mockControllerClass.clazz >> controllerClazz
69+
mockControllerClass.getPropertyValue('scaffold') >> scaffoldDomain
70+
mockWebRequest.controllerClass >> mockControllerClass
71+
}
72+
73+
void setupNamespaceController(String namespace = TEST_NAMESPACE) {
74+
mockControllerClass.namespace >> namespace
75+
}
76+
77+
GroovyPageView mockViewWithUrl(String url) {
78+
def view = Mock(GroovyPageView)
79+
view.url >> url
80+
return view
81+
}
82+
83+
void "test enableNamespaceViewDefaults defaults to false"() {
84+
expect:
85+
!resolver.enableNamespaceViewDefaults
86+
}
87+
88+
void "test scaffold value cache stores null values for non-scaffold controllers"() {
89+
given:
90+
setupScaffoldController(String)
91+
92+
when:
93+
def result = resolver.getScaffoldValue(mockControllerClass)
94+
95+
then:
96+
result == null
97+
resolver.scaffoldValueCache.containsKey(String)
98+
resolver.scaffoldValueCache.get(String) == ScaffoldingViewResolver.NULL_SCAFFOLD_VALUE
99+
}
100+
101+
void "test scaffold value cache returns cached value"() {
102+
given:
103+
setupScaffoldController(String)
104+
def cachedValue = String
105+
resolver.scaffoldValueCache.put(String, cachedValue)
106+
107+
when:
108+
def result = resolver.getScaffoldValue(mockControllerClass)
109+
110+
then:
111+
result == cachedValue
112+
0 * mockControllerClass.getPropertyValue(_) // Uses cache
113+
}
114+
115+
void "test scaffold value cache handles annotation"() {
116+
given:
117+
setupScaffoldController(TestScaffoldController)
118+
119+
when:
120+
def result = resolver.getScaffoldValue(mockControllerClass)
121+
122+
then:
123+
result == TestDomain
124+
resolver.scaffoldValueCache.containsKey(TestScaffoldController)
125+
}
126+
127+
void "test clearCache clears both view and scaffold caches"() {
128+
given:
129+
resolver.generatedViewCache.put("test", Mock(GroovyPageView))
130+
resolver.scaffoldValueCache.put(String, String)
131+
132+
when:
133+
resolver.clearCache()
134+
135+
then:
136+
resolver.generatedViewCache.isEmpty()
137+
resolver.scaffoldValueCache.isEmpty()
138+
}
139+
140+
void "test buildCacheKey includes view name"() {
141+
given:
142+
mockPageLocator.resolveViewFormat(TEST_VIEW_NAME) >> TEST_VIEW_NAME
143+
144+
when:
145+
def cacheKey = resolver.buildCacheKey(TEST_VIEW_NAME)
146+
147+
then:
148+
cacheKey != null
149+
cacheKey.contains(TEST_VIEW_NAME)
150+
}
151+
152+
void "test namespace controller without scaffold annotation returns null scaffold value"() {
153+
given:
154+
resolver.enableNamespaceViewDefaults = true
155+
setupScaffoldController(String)
156+
setupNamespaceController()
157+
158+
expect:
159+
resolver.getScaffoldValue(mockControllerClass) == null
160+
}
161+
162+
void "test tryGenerateScaffoldedView returns null for non-scaffold controller"() {
163+
given:
164+
setupScaffoldController(String)
165+
166+
when:
167+
def result = resolver.tryGenerateScaffoldedView(TEST_VIEW_NAME, mockControllerClass) { shortViewName ->
168+
Mock(Resource)
169+
}
170+
171+
then:
172+
result == null
173+
}
174+
175+
void "test tryGenerateScaffoldedView uses generated view cache"() {
176+
given:
177+
def cacheKey = "test-cache-key"
178+
def cachedView = Mock(GroovyPageView)
179+
resolver.generatedViewCache.put(cacheKey, cachedView)
180+
181+
expect:
182+
resolver.generatedViewCache.get(cacheKey) == cachedView
183+
}
184+
185+
void "test tryGenerateScaffoldedView returns null when resource does not exist"() {
186+
given:
187+
setupScaffoldController(TestScaffoldController, TestDomain)
188+
mockPageLocator.resolveViewFormat(TEST_VIEW_NAME) >> TEST_VIEW_NAME
189+
190+
when:
191+
def result = resolver.tryGenerateScaffoldedView(TEST_VIEW_NAME, mockControllerClass) { shortViewName ->
192+
def resource = Mock(Resource)
193+
resource.exists() >> false
194+
return resource
195+
}
196+
197+
then:
198+
result == null
199+
}
200+
201+
void "test RestfulServiceController annotation without AST transformation returns null"() {
202+
given:
203+
setupScaffoldController(TestRestfulServiceScaffoldController)
204+
205+
when:
206+
def result = resolver.getScaffoldValue(mockControllerClass)
207+
208+
then:
209+
// This test validates RAW annotation behavior (pre-AST transformation).
210+
// In real applications, ScaffoldingControllerInjector AST transformation extracts
211+
// the generic type from RestfulServiceController<TestDomain> and sets domain()
212+
// at compile time, so @Scaffold(RestfulServiceController<User>) DOES work.
213+
// See grails-test-examples/scaffolding for working integration tests.
214+
result == null
215+
resolver.scaffoldValueCache.containsKey(TestRestfulServiceScaffoldController)
216+
}
217+
218+
void "test Scaffold annotation with domain attribute works correctly"() {
219+
given:
220+
setupScaffoldController(TestScaffoldController, TestDomain)
221+
222+
when:
223+
def result = resolver.getScaffoldValue(mockControllerClass)
224+
225+
then:
226+
// This tests @Scaffold(domain = TestDomain) AND validates the post-AST behavior
227+
// of @Scaffold(RestfulServiceController<TestDomain>) since AST transformation
228+
// sets domain = TestDomain at compile time for both patterns
229+
result == TestDomain
230+
resolver.scaffoldValueCache.containsKey(TestScaffoldController)
231+
}
232+
233+
void "test namespace view URL detection identifies namespace-specific views"() {
234+
given:
235+
def namespaceView = mockViewWithUrl("/grails-app/views/${TEST_NAMESPACE}/event/index.gsp")
236+
def nonNamespaceView = mockViewWithUrl("/grails-app/views/event/index.gsp")
237+
setupNamespaceController()
238+
239+
expect:
240+
// Namespace view should contain namespace in URL
241+
namespaceView.url.contains("/${TEST_NAMESPACE}/")
242+
// Non-namespace view should not
243+
!nonNamespaceView.url.contains("/${TEST_NAMESPACE}/")
244+
}
245+
246+
void "test cache prevents repeated reflection for non-scaffold controllers"() {
247+
given:
248+
setupScaffoldController(String) // Non-scaffold controller
249+
250+
when: "First call performs reflection"
251+
def result1 = resolver.getScaffoldValue(mockControllerClass)
252+
253+
then:
254+
result1 == null
255+
resolver.scaffoldValueCache.get(String) == ScaffoldingViewResolver.NULL_SCAFFOLD_VALUE
256+
257+
when: "Second call uses cache"
258+
def result2 = resolver.getScaffoldValue(mockControllerClass)
259+
260+
then:
261+
result2 == null
262+
0 * mockControllerClass.getPropertyValue(_) // No reflection on second call
263+
}
264+
265+
// Test domain class for annotation testing
266+
static class TestDomain {}
267+
268+
@Scaffold(domain = TestDomain)
269+
static class TestScaffoldController {}
270+
271+
@Scaffold(RestfulServiceController<TestDomain>)
272+
static class TestRestfulServiceScaffoldController {}
273+
}

0 commit comments

Comments
 (0)