package okhttp3.internal.cache;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.wikipedia.dataclient.okhttp.HttpStatusException;
import org.wikipedia.dataclient.okhttp.OkHttpConnectionFactory;
import org.wikipedia.dataclient.okhttp.cache.SaveHeader;
import org.wikipedia.test.ImmediateExecutorService;
import org.wikipedia.test.MockWebServerTest;
import java.nio.charset.StandardCharsets;
import okhttp3.CacheControl;
import okhttp3.CacheDelegate;
import okhttp3.Dispatcher;
import okhttp3.Request;
import okhttp3.mockwebserver.MockResponse;
import okio.Buffer;
import okio.GzipSink;
import okio.Sink;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.notNullValue;
import static org.wikipedia.dataclient.okhttp.cache.DiskLruCacheUtil.okHttpResponseBodySize;
import static org.wikipedia.dataclient.okhttp.cache.DiskLruCacheUtil.okHttpResponseMetadataSize;
public class CacheDelegateInterceptorTest extends MockWebServerTest {
private static final String URL = "url";
@NonNull private final CacheDelegate netCache = new CacheDelegate(OkHttpConnectionFactory.NET_CACHE);
@NonNull private final CacheDelegate saveCache = new CacheDelegate(OkHttpConnectionFactory.SAVE_CACHE);
@Before public void setUp() throws Throwable {
super.setUp();
Request req = newRequest();
netCache.remove(req);
saveCache.remove(req);
}
// Both the network and saved cache are expected to be empty after each test's setUp().
@Test(expected = HttpStatusException.class) public void testAssumptionCacheIsEmptyAfterSetUp() throws Throwable {
Request req = newOnlyIfCachedRequest();
assertCached(netCache, req, false);
assertCached(saveCache, req, false);
executeRequest(req);
}
// The size on disk of an empty body is expected to be zero.
@Test public void testAssumptionCacheSizeEmptyBody() throws Throwable {
Request req = newRequest();
requestResponse("", req);
DiskLruCache.Snapshot snapshot = netCache.entry(req);
assertThat(okHttpResponseBodySize(snapshot), is(0L));
}
// The size on disk of a nonempty body is expected to be nonzero.
@Test public void testAssumptionCacheSizeNonemptyBody() throws Throwable {
Request req = newRequest();
requestResponse("A", req);
DiskLruCache.Snapshot snapshot = netCache.entry(req);
assertThat(okHttpResponseBodySize(snapshot), is(1L));
}
// The size on disk of OkHttp metadata is expected to be nonzero.
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Test public void testAssumptionCacheSizeMetadataIsNonzero() throws Throwable {
Request req = newRequest();
requestResponse("A", req);
DiskLruCache.Snapshot snapshot = netCache.entry(req);
// The size on disk of OkHttp metadata overhead is expected to be nonzero and necessary to
// consider when calculating disk usage for a page and all of it's resources so that more
// just the Content-Length header need be considered for each resource response.
assertThat(okHttpResponseMetadataSize(snapshot), notNullValue());
}
// Although OkHttp decompresses gzipped service responses seamlessly, the cache is expected to
// persist them in compressed form and report the compressed size, not the decompressed size.
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
@Test public void testAssumptionCacheSizeCompressedSizeIsReported() throws Throwable {
String interval = "0123456789"; // One cycle.
String body = StringUtils.repeat(interval, 100_000); // The body is many intervals.
Buffer buffer = new Buffer();
Sink sink = new GzipSink(buffer);
Buffer uncompressedBuffer = new Buffer().writeString(body, StandardCharsets.UTF_8);
long uncompressedSize = uncompressedBuffer.size();
sink.write(uncompressedBuffer, uncompressedBuffer.size());
sink.close();
// The compressed size is expected to be worse than one interval but at least 100x better
// than all intervals.
long compressedSize = buffer.size();
assertThat(compressedSize,
allOf(greaterThan((long) interval.length()), lessThan(uncompressedSize / 100L)));
// Enqueue a compressed response.
MockResponse serviceResponse = new MockResponse()
.addHeader("Content-Encoding", "gzip")
.setBody(buffer);
server().enqueue(serviceResponse);
Request req = newRequest();
String rsp = executeRequest(req);
server().takeRequest();
assertThat(rsp, is(body));
DiskLruCache.Snapshot snapshot = netCache.entry(req);
// The size on disk is expected to be the compressed size.
assertThat(okHttpResponseBodySize(snapshot), is(compressedSize));
}
@Test public void testInterceptWriteNetCacheNoHeader() throws Throwable {
Request req = newRequest();
requestResponse("0", req);
assertCached(netCache, req, true);
}
@Test public void testInterceptWriteSaveCacheNoHeader() throws Throwable {
Request req = newRequest();
requestResponse("0", req);
assertCached(saveCache, req, false);
}
@Test public void testInterceptWriteNetCacheSaveHeaderEnabled() throws Throwable {
Request req = newSaveEnabledRequest();
requestResponse("0", req);
assertCached(netCache, req, true);
}
@Test public void testInterceptWriteSaveCacheSaveHeaderEnabled() throws Throwable {
Request req = newSaveEnabledRequest();
requestResponse("0", req);
assertCached(saveCache, req, true);
}
@Test public void testInterceptWriteNetCacheSaveHeaderDisabled() throws Throwable {
Request req = newSaveDisabledRequest();
requestResponse("0", req);
assertCached(netCache, req, true);
}
@Test public void testInterceptWriteSaveCacheSaveHeaderDisabled() throws Throwable {
Request req = newSaveDisabledRequest();
requestResponse("0", req);
assertCached(saveCache, req, false);
}
@Test public void testInterceptReadNetCacheNoHeader() throws Throwable {
Request req = newRequest();
requestResponse("0", req);
saveCache.remove(req);
requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
}
@Test public void testInterceptReadSaveCacheNoHeader() throws Throwable {
Request req = newRequest();
requestResponse("0", newSaveEnabledRequest());
netCache.remove(req);
requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
}
@Test public void testInterceptReadNetCacheSaveHeaderEnabled() throws Throwable {
Request req = newSaveEnabledRequest();
requestResponse("0", req);
saveCache.remove(req);
requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
}
@Test public void testInterceptReadSaveCacheSaveHeaderEnabled() throws Throwable {
Request req = newSaveEnabledRequest();
requestResponse("0", req);
netCache.remove(req);
requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
}
@Test public void testInterceptReadNetCacheSaveHeaderDisabled() throws Throwable {
Request req = newSaveDisabledRequest();
requestResponse("0", req);
saveCache.remove(req);
requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
}
@Test public void testInterceptReadSaveCacheSaveHeaderDisabled() throws Throwable {
Request req = newSaveDisabledRequest();
requestResponse("0", newSaveEnabledRequest());
netCache.remove(req);
requestOnlyIfCachedResponse("0", newOnlyIfCachedRequest());
}
@Test public void testInterceptUpdateNetCacheNoHeader() throws Throwable {
Request req = newRequest();
requestResponse("0", req);
requestResponse("1", req);
saveCache.remove(req);
requestOnlyIfCachedResponse("1", newOnlyIfCachedRequest());
}
@Test public void testInterceptUpdateSaveCacheNoHeader() throws Throwable {
Request req = newRequest();
requestResponse("0", newSaveEnabledRequest());
requestResponse("1", req);
netCache.remove(req);
requestOnlyIfCachedResponse("1", newOnlyIfCachedRequest());
}
@Test public void testInterceptUpdateNetCacheSaveHeaderEnabled() throws Throwable {
Request req = newRequest();
requestResponse("0", req);
requestResponse("1", newSaveEnabledRequest());
saveCache.remove(req);
requestOnlyIfCachedResponse("1", newOnlyIfCachedRequest());
}
@Test public void testInterceptUpdateSaveCacheSaveHeaderEnabled() throws Throwable {
Request req = newRequest();
requestResponse("0", req);
requestResponse("1", newSaveEnabledRequest());
netCache.remove(req);
requestOnlyIfCachedResponse("1", newOnlyIfCachedRequest());
}
@Test public void testInterceptUpdateNetCacheSaveHeaderEnabledThenNoHeader() throws Throwable {
Request req = newRequest();
requestResponse("0", req);
requestResponse("1", newSaveEnabledRequest());
saveCache.remove(req);
requestResponse("2", req);
saveCache.remove(req);
requestOnlyIfCachedResponse("2", newOnlyIfCachedRequest());
}
@Test public void testInterceptUpdateSaveCacheSaveHeaderEnabledThenNoHeader() throws Throwable {
Request req = newRequest();
requestResponse("0", req);
requestResponse("1", newSaveEnabledRequest());
netCache.remove(req);
requestResponse("2", req);
netCache.remove(req);
requestOnlyIfCachedResponse("2", newOnlyIfCachedRequest());
}
@Test public void testInterceptErrorNetCache() throws Throwable {
Request req = newRequest();
requestResponse("0", req);
saveCache.remove(req);
requestResponseError("0", req);
}
@Test public void testInterceptErrorSaveCache() throws Throwable {
Request req = newSaveEnabledRequest();
requestResponse("0", req);
netCache.remove(req);
requestResponseError("0", req);
}
private void requestResponseError(@NonNull String body, @NonNull Request req) throws Throwable {
enqueue404();
String rsp = executeRequest(req);
server().takeRequest();
assertThat(rsp, is(body));
}
private void requestResponse(@NonNull String body, @NonNull Request req) throws Throwable {
server().enqueue(body);
String rsp = executeRequest(req);
server().takeRequest();
assertThat(rsp, is(body));
}
private void requestOnlyIfCachedResponse(@NonNull String body, @NonNull Request req) throws Throwable {
String rsp = executeRequest(req);
assertThat(rsp, is(body));
}
private String executeRequest(@NonNull Request req) throws Throwable {
// Note: raw non-Retrofit usage of OkHttp Requests requires that the Response body is read
// for the cache to be written.
return OkHttpConnectionFactory.getClient()
.newBuilder()
.dispatcher(new Dispatcher(new ImmediateExecutorService()))
.build()
.newCall(req).execute().body().string();
}
@NonNull private Request newOnlyIfCachedRequest() {
return newRequest().newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
}
@NonNull private Request newSaveEnabledRequest() {
return newRequest().newBuilder().header(SaveHeader.FIELD, SaveHeader.VAL_ENABLED).build();
}
@NonNull private Request newSaveDisabledRequest() {
return newRequest().newBuilder().header(SaveHeader.FIELD, SaveHeader.VAL_DISABLED).build();
}
@NonNull private Request newRequest() {
return new Request.Builder().url(server().getUrl(URL)).build();
}
private void assertCached(@NonNull CacheDelegate cacheDelegate, @NonNull Request req,
boolean cached) {
assertThat(cacheDelegate.isCached(req.url().toString()), is(cached));
}
}