1 /* 2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.webkit; 18 19 import static android.webkit.WebSettings.LOAD_DEFAULT; 20 21 import android.os.Build; 22 import android.webkit.WebStorage; 23 24 import androidx.test.ext.junit.runners.AndroidJUnit4; 25 import androidx.test.filters.MediumTest; 26 import androidx.test.filters.SdkSuppress; 27 import androidx.webkit.test.common.WebViewOnUiThread; 28 import androidx.webkit.test.common.WebkitUtils; 29 30 import org.junit.After; 31 import org.junit.Assert; 32 import org.junit.Before; 33 import org.junit.Test; 34 import org.junit.runner.RunWith; 35 36 import java.util.HashMap; 37 import java.util.Map; 38 import java.util.concurrent.CompletableFuture; 39 import java.util.concurrent.TimeUnit; 40 41 import okhttp3.mockwebserver.MockResponse; 42 import okhttp3.mockwebserver.MockWebServer; 43 import okhttp3.mockwebserver.QueueDispatcher; 44 import okhttp3.mockwebserver.RecordedRequest; 45 46 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) 47 48 @RunWith(AndroidJUnit4.class) 49 public class WebStorageTest { 50 WebViewOnUiThread mWebViewOnUiThread; 51 52 @Before setUp()53 public void setUp() { 54 mWebViewOnUiThread = new WebViewOnUiThread(); 55 mWebViewOnUiThread.getSettings().setCacheMode(LOAD_DEFAULT); 56 } 57 58 @After tearDown()59 public void tearDown() { 60 if (mWebViewOnUiThread != null) { 61 mWebViewOnUiThread.cleanUp(); 62 } 63 } 64 65 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) 66 @Test 67 @MediumTest testDeleteBrowsingDataDeletesCache()68 public void testDeleteBrowsingDataDeletesCache() throws Exception { 69 WebkitUtils.checkFeature(WebViewFeature.DELETE_BROWSING_DATA); 70 try (MockWebServer server = new MockWebServer()) { 71 CountingQueueDispatcher dispatcher = new CountingQueueDispatcher(); 72 server.setDispatcher(dispatcher); 73 server.start(); 74 75 MockResponse response = new MockResponse(); 76 response.setBody("response body"); 77 response.setHeader("Cache-Control", "max-age=604800"); 78 79 dispatcher.enqueueResponse(response); 80 81 String url = server.url("/").toString(); 82 83 // Load twice, but we should only see one request. 84 mWebViewOnUiThread.loadUrlAndWaitForCompletion(url); 85 mWebViewOnUiThread.loadUrlAndWaitForCompletion(url); 86 87 Assert.assertEquals( 88 "With cache headers, only one request should have made it to the server", 1, 89 dispatcher.getCountForPath("/")); 90 91 CompletableFuture<Void> resultFuture = new CompletableFuture<>(); 92 WebkitUtils.onMainThread( 93 () -> WebStorageCompat.deleteBrowsingData(WebStorage.getInstance(), 94 () -> resultFuture.complete(null))); 95 resultFuture.get(1, TimeUnit.SECONDS); 96 97 dispatcher.enqueueResponse(response); 98 mWebViewOnUiThread.loadUrlAndWaitForCompletion(url); 99 100 Assert.assertEquals("After deleting cache, a new request should be seen by the server", 101 2, dispatcher.getCountForPath("/")); 102 } 103 } 104 105 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.N) 106 @Test 107 @MediumTest testDeleteBrowsingDataForSiteDeletesCache()108 public void testDeleteBrowsingDataForSiteDeletesCache() throws Exception { 109 WebkitUtils.checkFeature(WebViewFeature.DELETE_BROWSING_DATA); 110 try (MockWebServer server = new MockWebServer(); 111 MockWebServer otherServer = new MockWebServer()) { 112 CountingQueueDispatcher dispatcher = new CountingQueueDispatcher(); 113 server.setDispatcher(dispatcher); 114 server.start(); 115 CountingQueueDispatcher otherDispatcher = new CountingQueueDispatcher(); 116 otherServer.setDispatcher(otherDispatcher); 117 otherServer.start(); 118 119 MockResponse response = new MockResponse(); 120 response.setBody("response body"); 121 response.setHeader("Cache-Control", "max-age=604800"); 122 123 dispatcher.enqueueResponse(response); 124 otherDispatcher.enqueueResponse(response); 125 126 String url = server.url("/").toString(); 127 Assert.assertEquals("localhost", server.getHostName()); 128 String otherUrl = otherServer.url("/").toString().replace("localhost", "127.0.0.1"); 129 130 // First load the other site twice and check that it was only requested once. 131 mWebViewOnUiThread.loadUrlAndWaitForCompletion(otherUrl); 132 mWebViewOnUiThread.loadUrlAndWaitForCompletion(otherUrl); 133 Assert.assertEquals(1, otherDispatcher.getCountForPath("/")); 134 135 // Load twice, but we should only see one request. 136 mWebViewOnUiThread.loadUrlAndWaitForCompletion(url); 137 mWebViewOnUiThread.loadUrlAndWaitForCompletion(url); 138 Assert.assertEquals( 139 "With cache headers, only one request should have made it to the server", 1, 140 dispatcher.getCountForPath("/")); 141 142 CompletableFuture<Void> resultFuture = new CompletableFuture<>(); 143 WebkitUtils.onMainThreadSync(() -> { 144 String actualSite = WebStorageCompat.deleteBrowsingDataForSite( 145 WebStorage.getInstance(), url, () -> resultFuture.complete(null)); 146 Assert.assertEquals("localhost", actualSite); 147 }); 148 resultFuture.get(1, TimeUnit.SECONDS); 149 150 dispatcher.enqueueResponse(response); 151 mWebViewOnUiThread.loadUrlAndWaitForCompletion(url); 152 Assert.assertEquals("After deleting cache, a new request should be seen by the server", 153 2, dispatcher.getCountForPath("/")); 154 155 mWebViewOnUiThread.loadUrlAndWaitForCompletion(otherUrl); 156 Assert.assertEquals("The other site should still be in cache", 1, 157 otherDispatcher.getCountForPath("/")); 158 159 } 160 } 161 162 /** 163 * {@link MockWebServer} dispatcher that counts the number of sent responses. 164 * 165 * @noinspection NewClassNamingConvention 166 */ 167 private static class CountingQueueDispatcher extends QueueDispatcher { 168 169 private final Map<String, Integer> mCounts = new HashMap<>(); 170 171 @Override dispatch(RecordedRequest request)172 public MockResponse dispatch(RecordedRequest request) throws InterruptedException { 173 mCounts.compute(request.getPath(), (s, count) -> count == null ? 1 : count + 1); 174 return super.dispatch(request); 175 } 176 getCountForPath(String path)177 int getCountForPath(String path) { 178 //noinspection DataFlowIssue 179 return mCounts.getOrDefault(path, 0); 180 } 181 } 182 } 183