1 | // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | package org.chromium.android_webview.test; |
6 | |
7 | import android.test.suitebuilder.annotation.SmallTest; |
8 | import android.util.Log; |
9 | import android.util.Pair; |
10 | |
11 | import org.chromium.android_webview.AndroidProtocolHandler; |
12 | import org.chromium.android_webview.AwContents; |
13 | import org.chromium.android_webview.InterceptedRequestData; |
14 | import org.chromium.android_webview.test.util.CommonResources; |
15 | import org.chromium.android_webview.test.util.JSUtils; |
16 | import org.chromium.base.test.util.Feature; |
17 | import org.chromium.base.test.util.TestFileUtil; |
18 | import org.chromium.content.browser.test.util.CallbackHelper; |
19 | import org.chromium.content.browser.test.util.TestCallbackHelperContainer.OnPageFinishedHelper; |
20 | import org.chromium.content.browser.test.util.TestCallbackHelperContainer.OnPageStartedHelper; |
21 | import org.chromium.content.browser.test.util.TestCallbackHelperContainer.OnReceivedErrorHelper; |
22 | import org.chromium.net.test.util.TestWebServer; |
23 | |
24 | import java.io.ByteArrayInputStream; |
25 | import java.io.InputStream; |
26 | import java.io.IOException; |
27 | import java.util.ArrayList; |
28 | import java.util.List; |
29 | import java.util.Random; |
30 | |
31 | /** |
32 | * Tests for the WebViewClient.shouldInterceptRequest() method. |
33 | */ |
34 | public class AwContentsClientShouldInterceptRequestTest extends AwTestBase { |
35 | |
36 | private static class TestAwContentsClient |
37 | extends org.chromium.android_webview.test.TestAwContentsClient { |
38 | |
39 | public static class ShouldInterceptRequestHelper extends CallbackHelper { |
40 | private List<String> mShouldInterceptRequestUrls = new ArrayList<String>(); |
41 | // This is read from the IO thread, so needs to be marked volatile. |
42 | private volatile InterceptedRequestData mShouldInterceptRequestReturnValue = null; |
43 | void setReturnValue(InterceptedRequestData value) { |
44 | mShouldInterceptRequestReturnValue = value; |
45 | } |
46 | public List<String> getUrls() { |
47 | assert getCallCount() > 0; |
48 | return mShouldInterceptRequestUrls; |
49 | } |
50 | public InterceptedRequestData getReturnValue() { |
51 | return mShouldInterceptRequestReturnValue; |
52 | } |
53 | public void notifyCalled(String url) { |
54 | mShouldInterceptRequestUrls.add(url); |
55 | notifyCalled(); |
56 | } |
57 | } |
58 | |
59 | public static class OnLoadResourceHelper extends CallbackHelper { |
60 | private String mUrl; |
61 | |
62 | public String getUrl() { |
63 | assert getCallCount() > 0; |
64 | return mUrl; |
65 | } |
66 | |
67 | public void notifyCalled(String url) { |
68 | mUrl = url; |
69 | notifyCalled(); |
70 | } |
71 | } |
72 | |
73 | @Override |
74 | public InterceptedRequestData shouldInterceptRequest(String url) { |
75 | InterceptedRequestData returnValue = mShouldInterceptRequestHelper.getReturnValue(); |
76 | mShouldInterceptRequestHelper.notifyCalled(url); |
77 | return returnValue; |
78 | } |
79 | |
80 | @Override |
81 | public void onLoadResource(String url) { |
82 | super.onLoadResource(url); |
83 | mOnLoadResourceHelper.notifyCalled(url); |
84 | } |
85 | |
86 | private ShouldInterceptRequestHelper mShouldInterceptRequestHelper; |
87 | private OnLoadResourceHelper mOnLoadResourceHelper; |
88 | |
89 | public TestAwContentsClient() { |
90 | mShouldInterceptRequestHelper = new ShouldInterceptRequestHelper(); |
91 | mOnLoadResourceHelper = new OnLoadResourceHelper(); |
92 | } |
93 | |
94 | public ShouldInterceptRequestHelper getShouldInterceptRequestHelper() { |
95 | return mShouldInterceptRequestHelper; |
96 | } |
97 | |
98 | public OnLoadResourceHelper getOnLoadResourceHelper() { |
99 | return mOnLoadResourceHelper; |
100 | } |
101 | } |
102 | |
103 | private String addPageToTestServer(TestWebServer webServer, String httpPath, String html) { |
104 | List<Pair<String, String>> headers = new ArrayList<Pair<String, String>>(); |
105 | headers.add(Pair.create("Content-Type", "text/html")); |
106 | headers.add(Pair.create("Cache-Control", "no-store")); |
107 | return webServer.setResponse(httpPath, html, headers); |
108 | } |
109 | |
110 | private String addAboutPageToTestServer(TestWebServer webServer) { |
111 | return addPageToTestServer(webServer, "/" + CommonResources.ABOUT_FILENAME, |
112 | CommonResources.ABOUT_HTML); |
113 | } |
114 | |
115 | private InterceptedRequestData stringToInterceptedRequestData(String input) throws Throwable { |
116 | final String mimeType = "text/html"; |
117 | final String encoding = "UTF-8"; |
118 | |
119 | return new InterceptedRequestData( |
120 | mimeType, encoding, new ByteArrayInputStream(input.getBytes(encoding))); |
121 | } |
122 | |
123 | private TestWebServer mWebServer; |
124 | private TestAwContentsClient mContentsClient; |
125 | private AwTestContainerView mTestContainerView; |
126 | private AwContents mAwContents; |
127 | private TestAwContentsClient.ShouldInterceptRequestHelper mShouldInterceptRequestHelper; |
128 | |
129 | @Override |
130 | protected void setUp() throws Exception { |
131 | super.setUp(); |
132 | |
133 | mContentsClient = new TestAwContentsClient(); |
134 | mTestContainerView = createAwTestContainerViewOnMainSync(mContentsClient); |
135 | mAwContents = mTestContainerView.getAwContents(); |
136 | mShouldInterceptRequestHelper = mContentsClient.getShouldInterceptRequestHelper(); |
137 | |
138 | mWebServer = new TestWebServer(false); |
139 | } |
140 | |
141 | @Override |
142 | protected void tearDown() throws Exception { |
143 | mWebServer.shutdown(); |
144 | super.tearDown(); |
145 | } |
146 | |
147 | @SmallTest |
148 | @Feature({"AndroidWebView"}) |
149 | public void testCalledWithCorrectUrl() throws Throwable { |
150 | final String aboutPageUrl = addAboutPageToTestServer(mWebServer); |
151 | |
152 | int callCount = mShouldInterceptRequestHelper.getCallCount(); |
153 | int onPageFinishedCallCount = mContentsClient.getOnPageFinishedHelper().getCallCount(); |
154 | |
155 | loadUrlAsync(mAwContents, aboutPageUrl); |
156 | |
157 | mShouldInterceptRequestHelper.waitForCallback(callCount); |
158 | assertEquals(1, mShouldInterceptRequestHelper.getUrls().size()); |
159 | assertEquals(aboutPageUrl, |
160 | mShouldInterceptRequestHelper.getUrls().get(0)); |
161 | |
162 | mContentsClient.getOnPageFinishedHelper().waitForCallback(onPageFinishedCallCount); |
163 | assertEquals(CommonResources.ABOUT_TITLE, getTitleOnUiThread(mAwContents)); |
164 | } |
165 | |
166 | @SmallTest |
167 | @Feature({"AndroidWebView"}) |
168 | public void testOnLoadResourceCalledWithCorrectUrl() throws Throwable { |
169 | final String aboutPageUrl = addAboutPageToTestServer(mWebServer); |
170 | final TestAwContentsClient.OnLoadResourceHelper onLoadResourceHelper = |
171 | mContentsClient.getOnLoadResourceHelper(); |
172 | |
173 | int callCount = onLoadResourceHelper.getCallCount(); |
174 | |
175 | loadUrlAsync(mAwContents, aboutPageUrl); |
176 | |
177 | onLoadResourceHelper.waitForCallback(callCount); |
178 | assertEquals(aboutPageUrl, onLoadResourceHelper.getUrl()); |
179 | } |
180 | |
181 | @SmallTest |
182 | @Feature({"AndroidWebView"}) |
183 | public void testDoesNotCrashOnInvalidData() throws Throwable { |
184 | final String aboutPageUrl = addAboutPageToTestServer(mWebServer); |
185 | |
186 | mShouldInterceptRequestHelper.setReturnValue( |
187 | new InterceptedRequestData("text/html", "UTF-8", null)); |
188 | int callCount = mShouldInterceptRequestHelper.getCallCount(); |
189 | loadUrlAsync(mAwContents, aboutPageUrl); |
190 | mShouldInterceptRequestHelper.waitForCallback(callCount); |
191 | |
192 | mShouldInterceptRequestHelper.setReturnValue( |
193 | new InterceptedRequestData(null, null, new ByteArrayInputStream(new byte[0]))); |
194 | callCount = mShouldInterceptRequestHelper.getCallCount(); |
195 | loadUrlAsync(mAwContents, aboutPageUrl); |
196 | mShouldInterceptRequestHelper.waitForCallback(callCount); |
197 | |
198 | mShouldInterceptRequestHelper.setReturnValue( |
199 | new InterceptedRequestData(null, null, null)); |
200 | callCount = mShouldInterceptRequestHelper.getCallCount(); |
201 | loadUrlAsync(mAwContents, aboutPageUrl); |
202 | mShouldInterceptRequestHelper.waitForCallback(callCount); |
203 | } |
204 | |
205 | private static class EmptyInputStream extends InputStream { |
206 | @Override |
207 | public int available() { |
208 | return 0; |
209 | } |
210 | |
211 | @Override |
212 | public int read() throws IOException { |
213 | return -1; |
214 | } |
215 | |
216 | @Override |
217 | public int read(byte b[]) throws IOException { |
218 | return -1; |
219 | } |
220 | |
221 | @Override |
222 | public int read(byte b[], int off, int len) throws IOException { |
223 | return -1; |
224 | } |
225 | |
226 | @Override |
227 | public long skip(long n) throws IOException { |
228 | if (n < 0) |
229 | throw new IOException("skipping negative number of bytes"); |
230 | return 0; |
231 | } |
232 | } |
233 | |
234 | @SmallTest |
235 | @Feature({"AndroidWebView"}) |
236 | public void testDoesNotCrashOnEmptyStream() throws Throwable { |
237 | final String aboutPageUrl = addAboutPageToTestServer(mWebServer); |
238 | |
239 | mShouldInterceptRequestHelper.setReturnValue( |
240 | new InterceptedRequestData("text/html", "UTF-8", new EmptyInputStream())); |
241 | int shouldInterceptRequestCallCount = mShouldInterceptRequestHelper.getCallCount(); |
242 | int onPageFinishedCallCount = mContentsClient.getOnPageFinishedHelper().getCallCount(); |
243 | |
244 | loadUrlAsync(mAwContents, aboutPageUrl); |
245 | |
246 | mShouldInterceptRequestHelper.waitForCallback(shouldInterceptRequestCallCount); |
247 | mContentsClient.getOnPageFinishedHelper().waitForCallback(onPageFinishedCallCount); |
248 | } |
249 | |
250 | @SmallTest |
251 | @Feature({"AndroidWebView"}) |
252 | public void testHttpStatusField() throws Throwable { |
253 | final String syncGetUrl = mWebServer.getResponseUrl("/intercept_me"); |
254 | final String syncGetJs = |
255 | "(function() {" + |
256 | " var xhr = new XMLHttpRequest();" + |
257 | " xhr.open('GET', '" + syncGetUrl + "', false);" + |
258 | " xhr.send(null);" + |
259 | " console.info('xhr.status = ' + xhr.status);" + |
260 | " return xhr.status;" + |
261 | "})();"; |
262 | enableJavaScriptOnUiThread(mAwContents); |
263 | |
264 | final String aboutPageUrl = addAboutPageToTestServer(mWebServer); |
265 | loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), aboutPageUrl); |
266 | |
267 | mShouldInterceptRequestHelper.setReturnValue( |
268 | new InterceptedRequestData("text/html", "UTF-8", null)); |
269 | assertEquals("404", |
270 | executeJavaScriptAndWaitForResult(mAwContents, mContentsClient, syncGetJs)); |
271 | |
272 | mShouldInterceptRequestHelper.setReturnValue( |
273 | new InterceptedRequestData("text/html", "UTF-8", new EmptyInputStream())); |
274 | assertEquals("200", |
275 | executeJavaScriptAndWaitForResult(mAwContents, mContentsClient, syncGetJs)); |
276 | } |
277 | |
278 | |
279 | private String makePageWithTitle(String title) { |
280 | return CommonResources.makeHtmlPageFrom("<title>" + title + "</title>", |
281 | "<div> The title is: " + title + " </div>"); |
282 | } |
283 | |
284 | @SmallTest |
285 | @Feature({"AndroidWebView"}) |
286 | public void testCanInterceptMainFrame() throws Throwable { |
287 | final String expectedTitle = "testShouldInterceptRequestCanInterceptMainFrame"; |
288 | final String expectedPage = makePageWithTitle(expectedTitle); |
289 | |
290 | mShouldInterceptRequestHelper.setReturnValue( |
291 | stringToInterceptedRequestData(expectedPage)); |
292 | |
293 | final String aboutPageUrl = addAboutPageToTestServer(mWebServer); |
294 | |
295 | loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), aboutPageUrl); |
296 | |
297 | assertEquals(expectedTitle, getTitleOnUiThread(mAwContents)); |
298 | assertEquals(0, mWebServer.getRequestCount("/" + CommonResources.ABOUT_FILENAME)); |
299 | } |
300 | |
301 | @SmallTest |
302 | @Feature({"AndroidWebView"}) |
303 | public void testDoesNotChangeReportedUrl() throws Throwable { |
304 | mShouldInterceptRequestHelper.setReturnValue( |
305 | stringToInterceptedRequestData(makePageWithTitle("some title"))); |
306 | |
307 | final String aboutPageUrl = addAboutPageToTestServer(mWebServer); |
308 | |
309 | loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), aboutPageUrl); |
310 | |
311 | assertEquals(aboutPageUrl, mContentsClient.getOnPageFinishedHelper().getUrl()); |
312 | assertEquals(aboutPageUrl, mContentsClient.getOnPageStartedHelper().getUrl()); |
313 | } |
314 | |
315 | @SmallTest |
316 | @Feature({"AndroidWebView"}) |
317 | public void testNullInputStreamCausesErrorForMainFrame() throws Throwable { |
318 | final OnReceivedErrorHelper onReceivedErrorHelper = |
319 | mContentsClient.getOnReceivedErrorHelper(); |
320 | |
321 | mShouldInterceptRequestHelper.setReturnValue( |
322 | new InterceptedRequestData("text/html", "UTF-8", null)); |
323 | |
324 | final String aboutPageUrl = addAboutPageToTestServer(mWebServer); |
325 | final int callCount = onReceivedErrorHelper.getCallCount(); |
326 | loadUrlAsync(mAwContents, aboutPageUrl); |
327 | onReceivedErrorHelper.waitForCallback(callCount); |
328 | assertEquals(0, mWebServer.getRequestCount("/" + CommonResources.ABOUT_FILENAME)); |
329 | } |
330 | |
331 | @SmallTest |
332 | @Feature({"AndroidWebView"}) |
333 | public void testCalledForImage() throws Throwable { |
334 | final String imagePath = "/" + CommonResources.FAVICON_FILENAME; |
335 | mWebServer.setResponseBase64(imagePath, |
336 | CommonResources.FAVICON_DATA_BASE64, CommonResources.getImagePngHeaders(true)); |
337 | final String pageWithImage = |
338 | addPageToTestServer(mWebServer, "/page_with_image.html", |
339 | CommonResources.getOnImageLoadedHtml(CommonResources.FAVICON_FILENAME)); |
340 | |
341 | int callCount = mShouldInterceptRequestHelper.getCallCount(); |
342 | loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), pageWithImage); |
343 | mShouldInterceptRequestHelper.waitForCallback(callCount, 2); |
344 | |
345 | assertEquals(2, mShouldInterceptRequestHelper.getUrls().size()); |
346 | assertTrue(mShouldInterceptRequestHelper.getUrls().get(1).endsWith( |
347 | CommonResources.FAVICON_FILENAME)); |
348 | } |
349 | |
350 | @SmallTest |
351 | @Feature({"AndroidWebView"}) |
352 | public void testCalledForIframe() throws Throwable { |
353 | final String aboutPageUrl = addAboutPageToTestServer(mWebServer); |
354 | final String pageWithIframe = addPageToTestServer(mWebServer, "/page_with_iframe.html", |
355 | CommonResources.makeHtmlPageFrom("", |
356 | "<iframe src=\"" + aboutPageUrl + "\"/>")); |
357 | |
358 | int callCount = mShouldInterceptRequestHelper.getCallCount(); |
359 | loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), pageWithIframe); |
360 | mShouldInterceptRequestHelper.waitForCallback(callCount, 2); |
361 | assertEquals(2, mShouldInterceptRequestHelper.getUrls().size()); |
362 | assertEquals(aboutPageUrl, mShouldInterceptRequestHelper.getUrls().get(1)); |
363 | } |
364 | |
365 | private void calledForUrlTemplate(final String url) throws Exception { |
366 | int callCount = mShouldInterceptRequestHelper.getCallCount(); |
367 | int onPageStartedCallCount = mContentsClient.getOnPageStartedHelper().getCallCount(); |
368 | loadUrlAsync(mAwContents, url); |
369 | mShouldInterceptRequestHelper.waitForCallback(callCount); |
370 | assertEquals(url, mShouldInterceptRequestHelper.getUrls().get(0)); |
371 | |
372 | mContentsClient.getOnPageStartedHelper().waitForCallback(onPageStartedCallCount); |
373 | assertEquals(onPageStartedCallCount + 1, |
374 | mContentsClient.getOnPageStartedHelper().getCallCount()); |
375 | } |
376 | |
377 | private void notCalledForUrlTemplate(final String url) throws Exception { |
378 | int callCount = mShouldInterceptRequestHelper.getCallCount(); |
379 | loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url); |
380 | // The intercepting must happen before onPageFinished. Since the IPC messages from the |
381 | // renderer should be delivered in order waiting for onPageFinished is sufficient to |
382 | // 'flush' any pending interception messages. |
383 | assertEquals(callCount, mShouldInterceptRequestHelper.getCallCount()); |
384 | } |
385 | |
386 | @SmallTest |
387 | @Feature({"AndroidWebView"}) |
388 | public void testCalledForUnsupportedSchemes() throws Throwable { |
389 | calledForUrlTemplate("foobar://resource/1"); |
390 | } |
391 | |
392 | @SmallTest |
393 | @Feature({"AndroidWebView"}) |
394 | public void testCalledForNonexistentFiles() throws Throwable { |
395 | calledForUrlTemplate("file:///somewhere/something"); |
396 | } |
397 | |
398 | @SmallTest |
399 | @Feature({"AndroidWebView"}) |
400 | public void testCalledForExistingFiles() throws Throwable { |
401 | final String tmpDir = getInstrumentation().getTargetContext().getCacheDir().getPath(); |
402 | final String fileName = tmpDir + "/testfile.html"; |
403 | final String title = "existing file title"; |
404 | TestFileUtil.deleteFile(fileName); // Remove leftover file if any. |
405 | TestFileUtil.createNewHtmlFile(fileName, title, ""); |
406 | final String existingFileUrl = "file://" + fileName; |
407 | |
408 | int callCount = mShouldInterceptRequestHelper.getCallCount(); |
409 | int onPageFinishedCallCount = mContentsClient.getOnPageFinishedHelper().getCallCount(); |
410 | loadUrlAsync(mAwContents, existingFileUrl); |
411 | mShouldInterceptRequestHelper.waitForCallback(callCount); |
412 | assertEquals(existingFileUrl, mShouldInterceptRequestHelper.getUrls().get(0)); |
413 | |
414 | mContentsClient.getOnPageFinishedHelper().waitForCallback(onPageFinishedCallCount); |
415 | assertEquals(title, getTitleOnUiThread(mAwContents)); |
416 | assertEquals(onPageFinishedCallCount + 1, |
417 | mContentsClient.getOnPageFinishedHelper().getCallCount()); |
418 | } |
419 | |
420 | @SmallTest |
421 | @Feature({"AndroidWebView"}) |
422 | public void testNotCalledForExistingResource() throws Throwable { |
423 | notCalledForUrlTemplate("file:///android_res/raw/resource_file.html"); |
424 | } |
425 | |
426 | @SmallTest |
427 | @Feature({"AndroidWebView"}) |
428 | public void testCalledForNonexistentResource() throws Throwable { |
429 | calledForUrlTemplate("file:///android_res/raw/no_file.html"); |
430 | } |
431 | |
432 | @SmallTest |
433 | @Feature({"AndroidWebView"}) |
434 | public void testNotCalledForExistingAsset() throws Throwable { |
435 | notCalledForUrlTemplate("file:///android_asset/asset_file.html"); |
436 | } |
437 | |
438 | @SmallTest |
439 | @Feature({"AndroidWebView"}) |
440 | public void testCalledForNonexistentAsset() throws Throwable { |
441 | calledForUrlTemplate("file:///android_res/raw/no_file.html"); |
442 | } |
443 | |
444 | @SmallTest |
445 | @Feature({"AndroidWebView"}) |
446 | public void testNotCalledForExistingContentUrl() throws Throwable { |
447 | final String contentResourceName = "target"; |
448 | final String existingContentUrl = TestContentProvider.createContentUrl(contentResourceName); |
449 | |
450 | notCalledForUrlTemplate(existingContentUrl); |
451 | |
452 | int contentRequestCount = TestContentProvider.getResourceRequestCount( |
453 | getInstrumentation().getTargetContext(), contentResourceName); |
454 | assertEquals(1, contentRequestCount); |
455 | } |
456 | |
457 | @SmallTest |
458 | @Feature({"AndroidWebView"}) |
459 | public void testCalledForNonexistentContentUrl() throws Throwable { |
460 | calledForUrlTemplate("content://org.chromium.webview.NoSuchProvider/foo"); |
461 | } |
462 | } |