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