// Copyright 2014 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "modules/cachestorage/Cache.h" #include "bindings/core/v8/ExceptionState.h" #include "bindings/core/v8/ScriptFunction.h" #include "bindings/core/v8/ScriptPromise.h" #include "bindings/core/v8/ScriptPromiseResolver.h" #include "bindings/core/v8/ScriptValue.h" #include "bindings/core/v8/V8Binding.h" #include "bindings/modules/v8/V8Request.h" #include "bindings/modules/v8/V8Response.h" #include "core/dom/Document.h" #include "core/frame/Frame.h" #include "core/testing/DummyPageHolder.h" #include "modules/fetch/FetchFormDataConsumerHandle.h" #include "modules/fetch/GlobalFetch.h" #include "modules/fetch/Request.h" #include "modules/fetch/Response.h" #include "modules/fetch/ResponseInit.h" #include "public/platform/WebURLResponse.h" #include "public/platform/modules/serviceworker/WebServiceWorkerCache.h" #include "testing/gtest/include/gtest/gtest.h" #include "wtf/OwnPtr.h" #include #include namespace blink { namespace { const char kNotImplementedString[] = "NotSupportedError: Method is not implemented."; class ScopedFetcherForTests final : public NoBaseWillBeGarbageCollectedFinalized, public GlobalFetch::ScopedFetcher { WILL_BE_USING_GARBAGE_COLLECTED_MIXIN(ScopedFetcherForTests); public: static PassOwnPtrWillBeRawPtr create() { return adoptPtrWillBeNoop(new ScopedFetcherForTests); } ScriptPromise fetch(ScriptState* scriptState, const RequestInfo& requestInfo, const Dictionary&, ExceptionState&) override { ++m_fetchCount; if (m_expectedUrl) { String fetchedUrl; if (requestInfo.isRequest()) EXPECT_EQ(*m_expectedUrl, requestInfo.getAsRequest()->url()); else EXPECT_EQ(*m_expectedUrl, requestInfo.getAsUSVString()); } if (m_response) { ScriptPromiseResolver* resolver = ScriptPromiseResolver::create(scriptState); const ScriptPromise promise = resolver->promise(); resolver->resolve(m_response); m_response = nullptr; return promise; } return ScriptPromise::reject(scriptState, V8ThrowException::createTypeError(scriptState->isolate(), "Unexpected call to fetch, no response available.")); } WeakPtrWillBeRawPtr weakPtr() { #if ENABLE(OILPAN) return this; #else return m_weakFactory.createWeakPtr(); #endif } // This does not take ownership of its parameter. The provided sample object is used to check the parameter when called. void setExpectedFetchUrl(const String* expectedUrl) { m_expectedUrl = expectedUrl; } void setResponse(Response* response) { m_response = response; } int fetchCount() const { return m_fetchCount; } DEFINE_INLINE_TRACE() { visitor->trace(m_response); GlobalFetch::ScopedFetcher::trace(visitor); } private: ScopedFetcherForTests() : m_fetchCount(0) , m_expectedUrl(nullptr) #if !ENABLE(OILPAN) , m_weakFactory(this) #endif { } int m_fetchCount; const String* m_expectedUrl; PersistentWillBeMember m_response; #if !ENABLE(OILPAN) WeakPtrFactory m_weakFactory; #endif }; // A test implementation of the WebServiceWorkerCache interface which returns a (provided) error for every operation, and optionally checks arguments // to methods against provided arguments. Also used as a base class for test specific caches. class ErrorWebCacheForTests : public WebServiceWorkerCache { public: ErrorWebCacheForTests(const WebServiceWorkerCacheError error) : m_error(error) , m_expectedUrl(0) , m_expectedQueryParams(0) , m_expectedBatchOperations(0) { } std::string getAndClearLastErrorWebCacheMethodCalled() { std::string old = m_lastErrorWebCacheMethodCalled; m_lastErrorWebCacheMethodCalled.clear(); return old; } // These methods do not take ownership of their parameter. They provide an optional sample object to check parameters against. void setExpectedUrl(const String* expectedUrl) { m_expectedUrl = expectedUrl; } void setExpectedQueryParams(const QueryParams* expectedQueryParams) { m_expectedQueryParams = expectedQueryParams; } void setExpectedBatchOperations(const WebVector* expectedBatchOperations) { m_expectedBatchOperations = expectedBatchOperations; } // From WebServiceWorkerCache: void dispatchMatch(CacheMatchCallbacks* callbacks, const WebServiceWorkerRequest& webRequest, const QueryParams& queryParams) override { m_lastErrorWebCacheMethodCalled = "dispatchMatch"; checkUrlIfProvided(webRequest.url()); checkQueryParamsIfProvided(queryParams); OwnPtr ownedCallbacks(adoptPtr(callbacks)); return callbacks->onError(m_error); } void dispatchMatchAll(CacheWithResponsesCallbacks* callbacks, const WebServiceWorkerRequest& webRequest, const QueryParams& queryParams) override { m_lastErrorWebCacheMethodCalled = "dispatchMatchAll"; checkUrlIfProvided(webRequest.url()); checkQueryParamsIfProvided(queryParams); OwnPtr ownedCallbacks(adoptPtr(callbacks)); return callbacks->onError(m_error); } void dispatchKeys(CacheWithRequestsCallbacks* callbacks, const WebServiceWorkerRequest* webRequest, const QueryParams& queryParams) override { m_lastErrorWebCacheMethodCalled = "dispatchKeys"; if (webRequest) { checkUrlIfProvided(webRequest->url()); checkQueryParamsIfProvided(queryParams); } OwnPtr ownedCallbacks(adoptPtr(callbacks)); return callbacks->onError(m_error); } void dispatchBatch(CacheBatchCallbacks* callbacks, const WebVector& batchOperations) override { m_lastErrorWebCacheMethodCalled = "dispatchBatch"; checkBatchOperationsIfProvided(batchOperations); OwnPtr ownedCallbacks(adoptPtr(callbacks)); return callbacks->onError(m_error); } protected: void checkUrlIfProvided(const KURL& url) { if (!m_expectedUrl) return; EXPECT_EQ(*m_expectedUrl, url); } void checkQueryParamsIfProvided(const QueryParams& queryParams) { if (!m_expectedQueryParams) return; CompareQueryParamsForTest(*m_expectedQueryParams, queryParams); } void checkBatchOperationsIfProvided(const WebVector& batchOperations) { if (!m_expectedBatchOperations) return; const WebVector expectedBatchOperations = *m_expectedBatchOperations; EXPECT_EQ(expectedBatchOperations.size(), batchOperations.size()); for (int i = 0, minsize = std::min(expectedBatchOperations.size(), batchOperations.size()); i < minsize; ++i) { EXPECT_EQ(expectedBatchOperations[i].operationType, batchOperations[i].operationType); const String expectedRequestUrl = KURL(expectedBatchOperations[i].request.url()); EXPECT_EQ(expectedRequestUrl, KURL(batchOperations[i].request.url())); const String expectedResponseUrl = KURL(expectedBatchOperations[i].response.url()); EXPECT_EQ(expectedResponseUrl, KURL(batchOperations[i].response.url())); CompareQueryParamsForTest(expectedBatchOperations[i].matchParams, batchOperations[i].matchParams); } } private: static void CompareQueryParamsForTest(const QueryParams& expectedQueryParams, const QueryParams& queryParams) { EXPECT_EQ(expectedQueryParams.ignoreSearch, queryParams.ignoreSearch); EXPECT_EQ(expectedQueryParams.ignoreMethod, queryParams.ignoreMethod); EXPECT_EQ(expectedQueryParams.ignoreVary, queryParams.ignoreVary); EXPECT_EQ(expectedQueryParams.cacheName, queryParams.cacheName); } const WebServiceWorkerCacheError m_error; const String* m_expectedUrl; const QueryParams* m_expectedQueryParams; const WebVector* m_expectedBatchOperations; std::string m_lastErrorWebCacheMethodCalled; }; class NotImplementedErrorCache : public ErrorWebCacheForTests { public: NotImplementedErrorCache() : ErrorWebCacheForTests(WebServiceWorkerCacheErrorNotImplemented) { } }; class CacheStorageTest : public ::testing::Test { public: CacheStorageTest() : m_page(DummyPageHolder::create(IntSize(1, 1))) { } Cache* createCache(ScopedFetcherForTests* fetcher, WebServiceWorkerCache* webCache) { return Cache::create(fetcher->weakPtr(), adoptPtr(webCache)); } ScriptState* getScriptState() { return ScriptState::forMainWorld(m_page->document().frame()); } ExecutionContext* getExecutionContext() { return getScriptState()->getExecutionContext(); } v8::Isolate* isolate() { return getScriptState()->isolate(); } v8::Local context() { return getScriptState()->context(); } Request* newRequestFromUrl(const String& url) { TrackExceptionState exceptionState; Request* request = Request::create(getScriptState(), url, exceptionState); EXPECT_FALSE(exceptionState.hadException()); return exceptionState.hadException() ? 0 : request; } // Convenience methods for testing the returned promises. ScriptValue getRejectValue(ScriptPromise& promise) { ScriptValue onReject; promise.then(UnreachableFunction::create(getScriptState()), TestFunction::create(getScriptState(), &onReject)); v8::MicrotasksScope::PerformCheckpoint(isolate()); return onReject; } std::string getRejectString(ScriptPromise& promise) { ScriptValue onReject = getRejectValue(promise); return toCoreString(onReject.v8Value()->ToString(context()).ToLocalChecked()).ascii().data(); } ScriptValue getResolveValue(ScriptPromise& promise) { ScriptValue onResolve; promise.then(TestFunction::create(getScriptState(), &onResolve), UnreachableFunction::create(getScriptState())); v8::MicrotasksScope::PerformCheckpoint(isolate()); return onResolve; } std::string getResolveString(ScriptPromise& promise) { ScriptValue onResolve = getResolveValue(promise); return toCoreString(onResolve.v8Value()->ToString(context()).ToLocalChecked()).ascii().data(); } ExceptionState& exceptionState() { return m_exceptionState; } private: // A ScriptFunction that creates a test failure if it is ever called. class UnreachableFunction : public ScriptFunction { public: static v8::Local create(ScriptState* scriptState) { UnreachableFunction* self = new UnreachableFunction(scriptState); return self->bindToV8Function(); } ScriptValue call(ScriptValue value) override { ADD_FAILURE() << "Unexpected call to a null ScriptFunction."; return value; } private: UnreachableFunction(ScriptState* scriptState) : ScriptFunction(scriptState) { } }; // A ScriptFunction that saves its parameter; used by tests to assert on correct // values being passed. class TestFunction : public ScriptFunction { public: static v8::Local create(ScriptState* scriptState, ScriptValue* outValue) { TestFunction* self = new TestFunction(scriptState, outValue); return self->bindToV8Function(); } ScriptValue call(ScriptValue value) override { ASSERT(!value.isEmpty()); *m_value = value; return value; } private: TestFunction(ScriptState* scriptState, ScriptValue* outValue) : ScriptFunction(scriptState), m_value(outValue) { } ScriptValue* m_value; }; // Lifetime is that of the text fixture. OwnPtr m_page; NonThrowableExceptionState m_exceptionState; }; RequestInfo stringToRequestInfo(const String& value) { RequestInfo info; info.setUSVString(value); return info; } RequestInfo requestToRequestInfo(Request* value) { RequestInfo info; info.setRequest(value); return info; } TEST_F(CacheStorageTest, Basics) { ScriptState::Scope scope(getScriptState()); OwnPtrWillBeRawPtr fetcher = ScopedFetcherForTests::create(); ErrorWebCacheForTests* testCache; Cache* cache = createCache(fetcher.get(), testCache = new NotImplementedErrorCache()); ASSERT(cache); const String url = "http://www.cachetest.org/"; CacheQueryOptions options; ScriptPromise matchPromise = cache->match(getScriptState(), stringToRequestInfo(url), options, exceptionState()); EXPECT_EQ(kNotImplementedString, getRejectString(matchPromise)); cache = createCache(fetcher.get(), testCache = new ErrorWebCacheForTests(WebServiceWorkerCacheErrorNotFound)); matchPromise = cache->match(getScriptState(), stringToRequestInfo(url), options, exceptionState()); ScriptValue scriptValue = getResolveValue(matchPromise); EXPECT_TRUE(scriptValue.isUndefined()); cache = createCache(fetcher.get(), testCache = new ErrorWebCacheForTests(WebServiceWorkerCacheErrorExists)); matchPromise = cache->match(getScriptState(), stringToRequestInfo(url), options, exceptionState()); EXPECT_EQ("InvalidAccessError: Entry already exists.", getRejectString(matchPromise)); } // Tests that arguments are faithfully passed on calls to Cache methods, except for methods which use batch operations, // which are tested later. TEST_F(CacheStorageTest, BasicArguments) { ScriptState::Scope scope(getScriptState()); OwnPtrWillBeRawPtr fetcher = ScopedFetcherForTests::create(); ErrorWebCacheForTests* testCache; Cache* cache = createCache(fetcher.get(), testCache = new NotImplementedErrorCache()); ASSERT(cache); const String url = "http://www.cache.arguments.test/"; testCache->setExpectedUrl(&url); WebServiceWorkerCache::QueryParams expectedQueryParams; expectedQueryParams.ignoreVary = true; expectedQueryParams.cacheName = "this is a cache name"; testCache->setExpectedQueryParams(&expectedQueryParams); CacheQueryOptions options; options.setIgnoreVary(1); options.setCacheName(expectedQueryParams.cacheName); Request* request = newRequestFromUrl(url); ASSERT(request); ScriptPromise matchResult = cache->match(getScriptState(), requestToRequestInfo(request), options, exceptionState()); EXPECT_EQ("dispatchMatch", testCache->getAndClearLastErrorWebCacheMethodCalled()); EXPECT_EQ(kNotImplementedString, getRejectString(matchResult)); ScriptPromise stringMatchResult = cache->match(getScriptState(), stringToRequestInfo(url), options, exceptionState()); EXPECT_EQ("dispatchMatch", testCache->getAndClearLastErrorWebCacheMethodCalled()); EXPECT_EQ(kNotImplementedString, getRejectString(stringMatchResult)); request = newRequestFromUrl(url); ASSERT(request); ScriptPromise matchAllResult = cache->matchAll(getScriptState(), requestToRequestInfo(request), options, exceptionState()); EXPECT_EQ("dispatchMatchAll", testCache->getAndClearLastErrorWebCacheMethodCalled()); EXPECT_EQ(kNotImplementedString, getRejectString(matchAllResult)); ScriptPromise stringMatchAllResult = cache->matchAll(getScriptState(), stringToRequestInfo(url), options, exceptionState()); EXPECT_EQ("dispatchMatchAll", testCache->getAndClearLastErrorWebCacheMethodCalled()); EXPECT_EQ(kNotImplementedString, getRejectString(stringMatchAllResult)); ScriptPromise keysResult1 = cache->keys(getScriptState(), exceptionState()); EXPECT_EQ("dispatchKeys", testCache->getAndClearLastErrorWebCacheMethodCalled()); EXPECT_EQ(kNotImplementedString, getRejectString(keysResult1)); request = newRequestFromUrl(url); ASSERT(request); ScriptPromise keysResult2 = cache->keys(getScriptState(), requestToRequestInfo(request), options, exceptionState()); EXPECT_EQ("dispatchKeys", testCache->getAndClearLastErrorWebCacheMethodCalled()); EXPECT_EQ(kNotImplementedString, getRejectString(keysResult2)); ScriptPromise stringKeysResult2 = cache->keys(getScriptState(), stringToRequestInfo(url), options, exceptionState()); EXPECT_EQ("dispatchKeys", testCache->getAndClearLastErrorWebCacheMethodCalled()); EXPECT_EQ(kNotImplementedString, getRejectString(stringKeysResult2)); } // Tests that arguments are faithfully passed to API calls that degrade to batch operations. TEST_F(CacheStorageTest, BatchOperationArguments) { ScriptState::Scope scope(getScriptState()); OwnPtrWillBeRawPtr fetcher = ScopedFetcherForTests::create(); ErrorWebCacheForTests* testCache; Cache* cache = createCache(fetcher.get(), testCache = new NotImplementedErrorCache()); ASSERT(cache); WebServiceWorkerCache::QueryParams expectedQueryParams; expectedQueryParams.cacheName = "this is another cache name"; testCache->setExpectedQueryParams(&expectedQueryParams); CacheQueryOptions options; options.setCacheName(expectedQueryParams.cacheName); const String url = "http://batch.operations.test/"; Request* request = newRequestFromUrl(url); ASSERT(request); WebServiceWorkerResponse webResponse; webResponse.setURL(KURL(ParsedURLString, url)); Response* response = Response::create(getExecutionContext(), webResponse); WebVector expectedDeleteOperations(size_t(1)); { WebServiceWorkerCache::BatchOperation deleteOperation; deleteOperation.operationType = WebServiceWorkerCache::OperationTypeDelete; request->populateWebServiceWorkerRequest(deleteOperation.request); deleteOperation.matchParams = expectedQueryParams; expectedDeleteOperations[0] = deleteOperation; } testCache->setExpectedBatchOperations(&expectedDeleteOperations); ScriptPromise deleteResult = cache->deleteFunction(getScriptState(), requestToRequestInfo(request), options, exceptionState()); EXPECT_EQ("dispatchBatch", testCache->getAndClearLastErrorWebCacheMethodCalled()); EXPECT_EQ(kNotImplementedString, getRejectString(deleteResult)); ScriptPromise stringDeleteResult = cache->deleteFunction(getScriptState(), stringToRequestInfo(url), options, exceptionState()); EXPECT_EQ("dispatchBatch", testCache->getAndClearLastErrorWebCacheMethodCalled()); EXPECT_EQ(kNotImplementedString, getRejectString(stringDeleteResult)); WebVector expectedPutOperations(size_t(1)); { WebServiceWorkerCache::BatchOperation putOperation; putOperation.operationType = WebServiceWorkerCache::OperationTypePut; request->populateWebServiceWorkerRequest(putOperation.request); response->populateWebServiceWorkerResponse(putOperation.response); expectedPutOperations[0] = putOperation; } testCache->setExpectedBatchOperations(&expectedPutOperations); request = newRequestFromUrl(url); ASSERT(request); ScriptPromise putResult = cache->put(getScriptState(), requestToRequestInfo(request), response->clone(exceptionState()), exceptionState()); EXPECT_EQ("dispatchBatch", testCache->getAndClearLastErrorWebCacheMethodCalled()); EXPECT_EQ(kNotImplementedString, getRejectString(putResult)); ScriptPromise stringPutResult = cache->put(getScriptState(), stringToRequestInfo(url), response, exceptionState()); EXPECT_EQ("dispatchBatch", testCache->getAndClearLastErrorWebCacheMethodCalled()); EXPECT_EQ(kNotImplementedString, getRejectString(stringPutResult)); // FIXME: test add & addAll. } class MatchTestCache : public NotImplementedErrorCache { public: MatchTestCache(WebServiceWorkerResponse& response) : m_response(response) { } // From WebServiceWorkerCache: void dispatchMatch(CacheMatchCallbacks* callbacks, const WebServiceWorkerRequest& webRequest, const QueryParams& queryParams) override { OwnPtr ownedCallbacks(adoptPtr(callbacks)); return callbacks->onSuccess(m_response); } private: WebServiceWorkerResponse& m_response; }; TEST_F(CacheStorageTest, MatchResponseTest) { ScriptState::Scope scope(getScriptState()); OwnPtrWillBeRawPtr fetcher = ScopedFetcherForTests::create(); const String requestUrl = "http://request.url/"; const String responseUrl = "http://match.response.test/"; WebServiceWorkerResponse webResponse; webResponse.setURL(KURL(ParsedURLString, responseUrl)); webResponse.setResponseType(WebServiceWorkerResponseTypeDefault); Cache* cache = createCache(fetcher.get(), new MatchTestCache(webResponse)); CacheQueryOptions options; ScriptPromise result = cache->match(getScriptState(), stringToRequestInfo(requestUrl), options, exceptionState()); ScriptValue scriptValue = getResolveValue(result); Response* response = V8Response::toImplWithTypeCheck(isolate(), scriptValue.v8Value()); ASSERT_TRUE(response); EXPECT_EQ(responseUrl, response->url()); } class KeysTestCache : public NotImplementedErrorCache { public: KeysTestCache(WebVector& requests) : m_requests(requests) { } void dispatchKeys(CacheWithRequestsCallbacks* callbacks, const WebServiceWorkerRequest* webRequest, const QueryParams& queryParams) override { OwnPtr ownedCallbacks(adoptPtr(callbacks)); return callbacks->onSuccess(m_requests); } private: WebVector& m_requests; }; TEST_F(CacheStorageTest, KeysResponseTest) { ScriptState::Scope scope(getScriptState()); OwnPtrWillBeRawPtr fetcher = ScopedFetcherForTests::create(); const String url1 = "http://first.request/"; const String url2 = "http://second.request/"; Vector expectedUrls(size_t(2)); expectedUrls[0] = url1; expectedUrls[1] = url2; WebVector webRequests(size_t(2)); webRequests[0].setURL(KURL(ParsedURLString, url1)); webRequests[1].setURL(KURL(ParsedURLString, url2)); Cache* cache = createCache(fetcher.get(), new KeysTestCache(webRequests)); ScriptPromise result = cache->keys(getScriptState(), exceptionState()); ScriptValue scriptValue = getResolveValue(result); Vector> requests = toImplArray>>(scriptValue.v8Value(), 0, isolate(), exceptionState()); EXPECT_EQ(expectedUrls.size(), requests.size()); for (int i = 0, minsize = std::min(expectedUrls.size(), requests.size()); i < minsize; ++i) { Request* request = V8Request::toImplWithTypeCheck(isolate(), requests[i]); EXPECT_TRUE(request); if (request) EXPECT_EQ(expectedUrls[i], request->url()); } } class MatchAllAndBatchTestCache : public NotImplementedErrorCache { public: MatchAllAndBatchTestCache(WebVector& responses) : m_responses(responses) { } void dispatchMatchAll(CacheWithResponsesCallbacks* callbacks, const WebServiceWorkerRequest& webRequest, const QueryParams& queryParams) override { OwnPtr ownedCallbacks(adoptPtr(callbacks)); return callbacks->onSuccess(m_responses); } void dispatchBatch(CacheBatchCallbacks* callbacks, const WebVector& batchOperations) override { OwnPtr ownedCallbacks(adoptPtr(callbacks)); return callbacks->onSuccess(); } private: WebVector& m_responses; }; TEST_F(CacheStorageTest, MatchAllAndBatchResponseTest) { ScriptState::Scope scope(getScriptState()); OwnPtrWillBeRawPtr fetcher = ScopedFetcherForTests::create(); const String url1 = "http://first.response/"; const String url2 = "http://second.response/"; Vector expectedUrls(size_t(2)); expectedUrls[0] = url1; expectedUrls[1] = url2; WebVector webResponses(size_t(2)); webResponses[0].setURL(KURL(ParsedURLString, url1)); webResponses[0].setResponseType(WebServiceWorkerResponseTypeDefault); webResponses[1].setURL(KURL(ParsedURLString, url2)); webResponses[1].setResponseType(WebServiceWorkerResponseTypeDefault); Cache* cache = createCache(fetcher.get(), new MatchAllAndBatchTestCache(webResponses)); CacheQueryOptions options; ScriptPromise result = cache->matchAll(getScriptState(), stringToRequestInfo("http://some.url/"), options, exceptionState()); ScriptValue scriptValue = getResolveValue(result); Vector> responses = toImplArray>>(scriptValue.v8Value(), 0, isolate(), exceptionState()); EXPECT_EQ(expectedUrls.size(), responses.size()); for (int i = 0, minsize = std::min(expectedUrls.size(), responses.size()); i < minsize; ++i) { Response* response = V8Response::toImplWithTypeCheck(isolate(), responses[i]); EXPECT_TRUE(response); if (response) EXPECT_EQ(expectedUrls[i], response->url()); } result = cache->deleteFunction(getScriptState(), stringToRequestInfo("http://some.url/"), options, exceptionState()); scriptValue = getResolveValue(result); EXPECT_TRUE(scriptValue.v8Value()->IsBoolean()); EXPECT_EQ(true, scriptValue.v8Value().As()->Value()); } TEST_F(CacheStorageTest, Add) { ScriptState::Scope scope(getScriptState()); OwnPtrWillBeRawPtr fetcher = ScopedFetcherForTests::create(); const String url = "http://www.cacheadd.test/"; const String contentType = "text/plain"; const String content = "hello cache"; ErrorWebCacheForTests* testCache; Cache* cache = createCache(fetcher.get(), testCache = new NotImplementedErrorCache()); fetcher->setExpectedFetchUrl(&url); Request* request = newRequestFromUrl(url); Response* response = Response::create(getExecutionContext(), FetchFormDataConsumerHandle::create(content), contentType, ResponseInit(), exceptionState()); fetcher->setResponse(response); WebVector expectedPutOperations(size_t(1)); { WebServiceWorkerCache::BatchOperation putOperation; putOperation.operationType = WebServiceWorkerCache::OperationTypePut; request->populateWebServiceWorkerRequest(putOperation.request); response->populateWebServiceWorkerResponse(putOperation.response); expectedPutOperations[0] = putOperation; } testCache->setExpectedBatchOperations(&expectedPutOperations); ScriptPromise addResult = cache->add(getScriptState(), requestToRequestInfo(request), exceptionState()); EXPECT_EQ(kNotImplementedString, getRejectString(addResult)); EXPECT_EQ(1, fetcher->fetchCount()); EXPECT_EQ("dispatchBatch", testCache->getAndClearLastErrorWebCacheMethodCalled()); } } // namespace } // namespace blink