22
22
import java .io .File ;
23
23
import java .net .MalformedURLException ;
24
24
import java .net .URL ;
25
+ import java .util .Optional ;
26
+ import java .util .concurrent .CompletableFuture ;
27
+ import java .util .concurrent .ExecutorService ;
28
+ import java .util .concurrent .Executors ;
29
+ import java .util .concurrent .TimeUnit ;
25
30
26
31
import jakarta .servlet .ServletContext ;
27
32
28
33
import org .apache .commons .io .FileUtils ;
34
+ import org .apache .commons .lang3 .mutable .Mutable ;
35
+ import org .apache .commons .lang3 .mutable .MutableObject ;
29
36
import org .junit .jupiter .api .AfterEach ;
30
37
import org .junit .jupiter .api .BeforeEach ;
31
38
import org .junit .jupiter .api .Test ;
32
39
import org .junit .jupiter .api .extension .RegisterExtension ;
40
+ import org .junit .jupiter .params .ParameterizedTest ;
41
+ import org .junit .jupiter .params .provider .ValueSource ;
42
+ import org .mockito .Mock ;
33
43
import org .slf4j .Logger ;
34
- import org .xwiki .component .embed .EmbeddableComponentManager ;
44
+ import org .xwiki .cache .Cache ;
45
+ import org .xwiki .cache .CacheControl ;
46
+ import org .xwiki .cache .CacheManager ;
35
47
import org .xwiki .component .util .ReflectionUtils ;
36
- import org .xwiki .environment .Environment ;
37
48
import org .xwiki .test .junit5 .LogCaptureExtension ;
49
+ import org .xwiki .test .junit5 .mockito .ComponentTest ;
50
+ import org .xwiki .test .junit5 .mockito .InjectMockComponents ;
51
+ import org .xwiki .test .junit5 .mockito .MockComponent ;
38
52
39
53
import static org .junit .jupiter .api .Assertions .assertEquals ;
40
54
import static org .junit .jupiter .api .Assertions .assertNull ;
41
55
import static org .junit .jupiter .api .Assertions .assertThrows ;
42
- import static org .mockito .Mockito .*;
56
+ import static org .mockito .ArgumentMatchers .any ;
57
+ import static org .mockito .Mockito .doAnswer ;
58
+ import static org .mockito .Mockito .doReturn ;
59
+ import static org .mockito .Mockito .mock ;
60
+ import static org .mockito .Mockito .never ;
61
+ import static org .mockito .Mockito .times ;
62
+ import static org .mockito .Mockito .verify ;
63
+ import static org .mockito .Mockito .verifyNoInteractions ;
64
+ import static org .mockito .Mockito .when ;
43
65
44
66
/**
45
67
* Unit tests for {@link ServletEnvironment}.
46
68
*
47
69
* @version $Id$
48
70
* @since 3.5M1
49
71
*/
72
+ @ SuppressWarnings ({ "checkstyle:ClassFanOutComplexity" , "checkstyle:MultipleStringLiterals" })
73
+ @ ComponentTest
50
74
class ServletEnvironmentTest
51
75
{
52
76
private File servletTmpDir ;
53
77
54
78
private File systemTmpDir ;
55
79
80
+ @ MockComponent
81
+ private CacheManager cacheManager ;
82
+
83
+ @ Mock
84
+ private Cache <Optional <URL >> cache ;
85
+
86
+ @ MockComponent
87
+ private CacheControl cacheControl ;
88
+
89
+ @ InjectMockComponents
56
90
private ServletEnvironment environment ;
57
91
58
92
@ RegisterExtension
@@ -64,9 +98,7 @@ public void setUp() throws Exception
64
98
this .servletTmpDir = new File (System .getProperty ("java.io.tmpdir" ), "ServletEnvironmentTest-tmpDir" );
65
99
this .systemTmpDir = new File (System .getProperty ("java.io.tmpdir" ), "xwiki-temp" );
66
100
67
- EmbeddableComponentManager ecm = new EmbeddableComponentManager ();
68
- ecm .initialize (getClass ().getClassLoader ());
69
- this .environment = ecm .getInstance (Environment .class );
101
+ doReturn (this .cache ).when (this .cacheManager ).createNewCache (any ());
70
102
}
71
103
72
104
@ AfterEach
@@ -90,10 +122,15 @@ void getResourceOk() throws Exception
90
122
{
91
123
ServletContext servletContext = mock (ServletContext .class );
92
124
this .environment .setServletContext (servletContext );
93
- when (servletContext .getResource ("/test" )).thenReturn (new URL ("file:/path/../test" ));
94
-
95
- assertEquals (new URL ("file:/test" ), this .environment .getResource ("/test" ));
96
- verify (servletContext ).getResource ("/test" );
125
+ String resourceName = "/test" ;
126
+ when (servletContext .getResource (resourceName )).thenReturn (new URL ("file:/path/../test" ));
127
+
128
+ URL expectedURL = new URL ("file:/test" );
129
+ assertEquals (expectedURL , this .environment .getResource (resourceName ));
130
+ verify (servletContext ).getResource (resourceName );
131
+ verify (this .cache ).set (resourceName , Optional .of (expectedURL ));
132
+ // As cache control returns false, the cache shouldn't be read.
133
+ verify (this .cache , never ()).get (any ());
97
134
}
98
135
99
136
@ Test
@@ -104,25 +141,132 @@ void getResourceAsStreamOk() throws Exception
104
141
this .environment .getResourceAsStream ("/test" );
105
142
106
143
verify (servletContext ).getResourceAsStream ("/test" );
144
+ // No cache for streams.
145
+ verifyNoInteractions (this .cache );
107
146
}
108
147
109
148
@ Test
110
149
void getResourceNotExisting () throws Exception
111
150
{
112
151
ServletContext servletContext = mock (ServletContext .class );
113
152
this .environment .setServletContext (servletContext );
114
- assertNull (this .environment .getResource ("unknown resource" ));
153
+ String resourceName = "unknown resource" ;
154
+ assertNull (this .environment .getResource (resourceName ));
155
+ verify (this .cache ).set (resourceName , Optional .empty ());
156
+ // As cache control returns false, the cache shouldn't be read.
157
+ verify (this .cache , never ()).get (any ());
115
158
}
116
159
117
160
@ Test
118
161
void getResourceWhenMalformedURLException () throws Exception
119
162
{
120
163
ServletContext servletContext = mock (ServletContext .class );
121
- when (servletContext .getResource ("bad resource" )).thenThrow (new MalformedURLException ("invalid url" ));
164
+ String resourceName = "bad resource" ;
165
+ when (servletContext .getResource (resourceName )).thenThrow (new MalformedURLException ("invalid url" ));
122
166
this .environment .setServletContext (servletContext );
123
- assertNull (this .environment .getResource ("bad resource" ));
167
+ assertNull (this .environment .getResource (resourceName ));
124
168
assertEquals ("Error getting resource [bad resource] because of invalid path format. Reason: [invalid url]" ,
125
169
logCapture .getMessage (0 ));
170
+ verify (this .cache ).set (resourceName , Optional .empty ());
171
+ // As cache control returns false, the cache shouldn't be read.
172
+ verify (this .cache , never ()).get (any ());
173
+ }
174
+
175
+ @ Test
176
+ void getResourceNotExistingFromCache ()
177
+ {
178
+ String resourceName = "unknown resource" ;
179
+ when (this .cacheControl .isCacheReadAllowed ()).thenReturn (true );
180
+ when (this .cache .get (resourceName )).thenReturn (Optional .empty ());
181
+ assertNull (this .environment .getResource (resourceName ));
182
+ verify (this .cache ).get (resourceName );
183
+ verify (this .cache , never ()).set (any (), any ());
184
+ }
185
+
186
+ @ Test
187
+ void getResourceExistingFromCache ()
188
+ {
189
+ String resourceName = "known resource" ;
190
+ URL expectedURL = mock (URL .class );
191
+ when (this .cacheControl .isCacheReadAllowed ()).thenReturn (true );
192
+ when (this .cache .get (resourceName )).thenReturn (Optional .of (expectedURL ));
193
+ assertEquals (expectedURL , this .environment .getResource (resourceName ));
194
+ verify (this .cache ).get (resourceName );
195
+ verify (this .cache , never ()).set (any (), any ());
196
+ }
197
+
198
+ @ ParameterizedTest
199
+ @ ValueSource (booleans = { true , false })
200
+ void getResourceWithRecursiveCallInCacheInitialization (boolean cached ) throws Exception
201
+ {
202
+ ServletContext servletContext = mock (ServletContext .class );
203
+ String resourceName = "/resource" ;
204
+ when (servletContext .getResource (resourceName )).thenReturn (new URL ("file:/path/../resource" ));
205
+ this .environment .setServletContext (servletContext );
206
+
207
+ when (this .cacheControl .isCacheReadAllowed ()).thenReturn (true );
208
+
209
+ URL cachedURL = mock ();
210
+ if (cached ) {
211
+ when (this .cache .get (resourceName )).thenReturn (Optional .of (cachedURL ));
212
+ }
213
+
214
+ // Two futures to synchronize the background thread that creates the cache.
215
+ CompletableFuture <Void > arrivedInCreateCacheFuture = new CompletableFuture <>();
216
+ CompletableFuture <Void > blockCreateCacheFuture = new CompletableFuture <>();
217
+
218
+ Mutable <URL > recursiveURL = new MutableObject <>();
219
+
220
+ doAnswer (invocationOnMock -> {
221
+ // Signal that the background thread arrived in the cache creation call.
222
+ arrivedInCreateCacheFuture .complete (null );
223
+ recursiveURL .setValue (this .environment .getResource (resourceName ));
224
+ // Wait until the main thread unblocks the cache creation (but don't wait forever just to be safe).
225
+ blockCreateCacheFuture .get (20 , TimeUnit .SECONDS );
226
+ return this .cache ;
227
+ }).when (this .cacheManager ).createNewCache (any ());
228
+
229
+ // Launch a background thread so we can test blocking in the creation of the cache.
230
+ ExecutorService executor = Executors .newFixedThreadPool (1 );
231
+
232
+ try {
233
+ CompletableFuture <URL > outerCall =
234
+ CompletableFuture .supplyAsync (() -> this .environment .getResource (resourceName ), executor );
235
+
236
+ // Wait for the background thread to arrive in the cache creation call (but don't wait forever just to be
237
+ // safe).
238
+ arrivedInCreateCacheFuture .get (20 , TimeUnit .SECONDS );
239
+
240
+ URL expectedURL = new URL ("file:/resource" );
241
+ // Ensure that the cache creation doesn't block getting the resource URL.
242
+ assertEquals (expectedURL , this .environment .getResource (resourceName ));
243
+
244
+ // Unblock the cache creation call.
245
+ blockCreateCacheFuture .complete (null );
246
+
247
+ // Ensure that the blocked call now got the value, too. Again, don't wait forever just in case.
248
+ URL actualOuterURL = outerCall .get (20 , TimeUnit .SECONDS );
249
+ if (cached ) {
250
+ assertEquals (cachedURL , actualOuterURL );
251
+ } else {
252
+ assertEquals (expectedURL , actualOuterURL );
253
+ }
254
+
255
+ // Assert that the recursive call also got the URL.
256
+ assertEquals (expectedURL , recursiveURL .getValue ());
257
+
258
+ // Only the outer call should have accessed the cache. If the cache didn't return the value, it should
259
+ // have been stored.
260
+ verify (this .cache ).get (resourceName );
261
+ if (cached ) {
262
+ verify (this .cache , never ()).set (any (), any ());
263
+ } else {
264
+ verify (this .cache ).set (resourceName , Optional .of (expectedURL ));
265
+ }
266
+ verify (servletContext , times (cached ? 2 : 3 )).getResource (resourceName );
267
+ } finally {
268
+ executor .shutdownNow ();
269
+ }
126
270
}
127
271
128
272
@ Test
0 commit comments