|  | // 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 <stdint.h> | 
|  |  | 
|  | #include <string> | 
|  |  | 
|  | #include "base/bind.h" | 
|  | #include "base/bind_helpers.h" | 
|  | #include "base/location.h" | 
|  | #include "base/pickle.h" | 
|  | #include "base/run_loop.h" | 
|  | #include "base/single_thread_task_runner.h" | 
|  | #include "base/stl_util.h" | 
|  | #include "base/test/scoped_task_environment.h" | 
|  | #include "base/threading/thread_task_runner_handle.h" | 
|  | #include "content/browser/appcache/appcache_response.h" | 
|  | #include "content/browser/appcache/appcache_service_impl.h" | 
|  | #include "content/browser/appcache/mock_appcache_storage.h" | 
|  | #include "net/base/completion_once_callback.h" | 
|  | #include "net/base/io_buffer.h" | 
|  | #include "net/http/http_response_headers.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  |  | 
|  | namespace content { | 
|  | namespace { | 
|  |  | 
|  | const int64_t kMockGroupId = 1; | 
|  | const int64_t kMockCacheId = 1; | 
|  | const int64_t kMockResponseId = 1; | 
|  | const int64_t kMissingCacheId = 5; | 
|  | const int64_t kMissingResponseId = 5; | 
|  | const char kMockHeaders[] = | 
|  | "HTTP/1.0 200 OK\0Content-Length: 5\0\0"; | 
|  | const char kMockBody[] = "Hello"; | 
|  | const int kMockBodySize = 5; | 
|  |  | 
|  | class MockResponseReader : public AppCacheResponseReader { | 
|  | public: | 
|  | MockResponseReader(int64_t response_id, | 
|  | net::HttpResponseInfo* info, | 
|  | int info_size, | 
|  | const char* data, | 
|  | int data_size) | 
|  | : AppCacheResponseReader(response_id, /*disk_cache=*/nullptr), | 
|  | info_(info), | 
|  | info_size_(info_size), | 
|  | data_(data), | 
|  | data_size_(data_size) {} | 
|  | void ReadInfo(HttpResponseInfoIOBuffer* info_buf, | 
|  | OnceCompletionCallback callback) override { | 
|  | info_buffer_ = info_buf; | 
|  | callback_ = std::move(callback);  // Cleared on completion. | 
|  |  | 
|  | int rv = info_.get() ? info_size_ : net::ERR_FAILED; | 
|  | info_buffer_->http_info.reset(info_.release()); | 
|  | info_buffer_->response_data_size = data_size_; | 
|  | ScheduleUserCallback(rv); | 
|  | } | 
|  | void ReadData(net::IOBuffer* buf, | 
|  | int buf_len, | 
|  | OnceCompletionCallback callback) override { | 
|  | buffer_ = buf; | 
|  | buffer_len_ = buf_len; | 
|  | callback_ = std::move(callback);  // Cleared on completion. | 
|  |  | 
|  | if (!data_) { | 
|  | ScheduleUserCallback(net::ERR_CACHE_READ_FAILURE); | 
|  | return; | 
|  | } | 
|  | DCHECK(buf_len >= data_size_); | 
|  | memcpy(buf->data(), data_, data_size_); | 
|  | ScheduleUserCallback(data_size_); | 
|  | data_size_ = 0; | 
|  | } | 
|  |  | 
|  | private: | 
|  | void ScheduleUserCallback(int result) { | 
|  | base::ThreadTaskRunnerHandle::Get()->PostTask( | 
|  | FROM_HERE, | 
|  | base::BindOnce(&MockResponseReader::InvokeUserCompletionCallback, | 
|  | weak_factory_.GetWeakPtr(), result)); | 
|  | } | 
|  |  | 
|  | std::unique_ptr<net::HttpResponseInfo> info_; | 
|  | int info_size_; | 
|  | const char* data_; | 
|  | int data_size_; | 
|  | }; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  |  | 
|  | class AppCacheServiceImplTest : public testing::Test { | 
|  | public: | 
|  | AppCacheServiceImplTest() | 
|  | : kOriginURL("http://hello/"), | 
|  | kOrigin(url::Origin::Create(kOriginURL)), | 
|  | kManifestUrl(kOriginURL.Resolve("manifest")), | 
|  | service_(new AppCacheServiceImpl(nullptr)), | 
|  | delete_result_(net::OK), | 
|  | delete_completion_count_(0) { | 
|  | // Setup to use mock storage. | 
|  | service_->storage_.reset(new MockAppCacheStorage(service_.get())); | 
|  | } | 
|  |  | 
|  | void OnDeleteAppCachesComplete(int result) { | 
|  | delete_result_ = result; | 
|  | ++delete_completion_count_; | 
|  | } | 
|  |  | 
|  | MockAppCacheStorage* mock_storage() { | 
|  | return static_cast<MockAppCacheStorage*>(service_->storage()); | 
|  | } | 
|  |  | 
|  | void ResetStorage() { | 
|  | service_->storage_.reset(new MockAppCacheStorage(service_.get())); | 
|  | } | 
|  |  | 
|  | bool IsGroupStored(const GURL& manifest_url) { | 
|  | return mock_storage()->IsGroupForManifestStored(manifest_url); | 
|  | } | 
|  |  | 
|  | int CountPendingHelpers() { | 
|  | return service_->pending_helpers_.size(); | 
|  | } | 
|  |  | 
|  | void SetupMockGroup() { | 
|  | std::unique_ptr<net::HttpResponseInfo> info(MakeMockResponseInfo()); | 
|  | const int kMockInfoSize = GetResponseInfoSize(info.get()); | 
|  |  | 
|  | // Create a mock group, cache, and entry and stuff them into mock storage. | 
|  | scoped_refptr<AppCacheGroup> group( | 
|  | new AppCacheGroup(service_->storage(), kManifestUrl, kMockGroupId)); | 
|  | scoped_refptr<AppCache> cache( | 
|  | new AppCache(service_->storage(), kMockCacheId)); | 
|  | cache->AddEntry( | 
|  | kManifestUrl, | 
|  | AppCacheEntry(AppCacheEntry::MANIFEST, kMockResponseId, | 
|  | kMockInfoSize + kMockBodySize)); | 
|  | cache->set_complete(true); | 
|  | group->AddCache(cache.get()); | 
|  | mock_storage()->AddStoredGroup(group.get()); | 
|  | mock_storage()->AddStoredCache(cache.get()); | 
|  | } | 
|  |  | 
|  | void SetupMockReader( | 
|  | bool valid_info, bool valid_data, bool valid_size) { | 
|  | net::HttpResponseInfo* info = valid_info ? MakeMockResponseInfo() : nullptr; | 
|  | int info_size = info ? GetResponseInfoSize(info) : 0; | 
|  | const char* data = valid_data ? kMockBody : nullptr; | 
|  | int data_size = valid_size ? kMockBodySize : 3; | 
|  | mock_storage()->SimulateResponseReader( | 
|  | new MockResponseReader(kMockResponseId, info, info_size, | 
|  | data, data_size)); | 
|  | } | 
|  |  | 
|  | net::HttpResponseInfo* MakeMockResponseInfo() { | 
|  | net::HttpResponseInfo* info = new net::HttpResponseInfo; | 
|  | info->request_time = base::Time::Now(); | 
|  | info->response_time = base::Time::Now(); | 
|  | info->was_cached = false; | 
|  | info->headers = new net::HttpResponseHeaders( | 
|  | std::string(kMockHeaders, base::size(kMockHeaders))); | 
|  | return info; | 
|  | } | 
|  |  | 
|  | int GetResponseInfoSize(const net::HttpResponseInfo* info) { | 
|  | base::Pickle pickle; | 
|  | return PickleResponseInfo(&pickle, info); | 
|  | } | 
|  |  | 
|  | int PickleResponseInfo(base::Pickle* pickle, | 
|  | const net::HttpResponseInfo* info) { | 
|  | const bool kSkipTransientHeaders = true; | 
|  | const bool kTruncated = false; | 
|  | info->Persist(pickle, kSkipTransientHeaders, kTruncated); | 
|  | return pickle->size(); | 
|  | } | 
|  |  | 
|  | net::CompletionOnceCallback deletion_callback() { | 
|  | return base::BindOnce(&AppCacheServiceImplTest::OnDeleteAppCachesComplete, | 
|  | base::Unretained(this)); | 
|  | } | 
|  |  | 
|  | const GURL kOriginURL; | 
|  | const url::Origin kOrigin; | 
|  | const GURL kManifestUrl; | 
|  |  | 
|  | base::test::ScopedTaskEnvironment scoped_task_environment_; | 
|  | std::unique_ptr<AppCacheServiceImpl> service_; | 
|  | int delete_result_; | 
|  | int delete_completion_count_; | 
|  | }; | 
|  |  | 
|  | TEST_F(AppCacheServiceImplTest, DeleteAppCachesForOrigin) { | 
|  | // Without giving mock storage simiulated info, should fail. | 
|  | service_->DeleteAppCachesForOrigin(kOrigin, deletion_callback()); | 
|  | EXPECT_EQ(0, delete_completion_count_); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | EXPECT_EQ(1, delete_completion_count_); | 
|  | EXPECT_EQ(net::ERR_FAILED, delete_result_); | 
|  | delete_completion_count_ = 0; | 
|  |  | 
|  | // Should succeed given an empty info collection. | 
|  | mock_storage()->SimulateGetAllInfo(new content::AppCacheInfoCollection); | 
|  | service_->DeleteAppCachesForOrigin(kOrigin, deletion_callback()); | 
|  | EXPECT_EQ(0, delete_completion_count_); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | EXPECT_EQ(1, delete_completion_count_); | 
|  | EXPECT_EQ(net::OK, delete_result_); | 
|  | delete_completion_count_ = 0; | 
|  |  | 
|  | scoped_refptr<AppCacheInfoCollection> info(new AppCacheInfoCollection); | 
|  |  | 
|  | // Should succeed given a non-empty info collection. | 
|  | AppCacheInfo mock_manifest_1; | 
|  | AppCacheInfo mock_manifest_2; | 
|  | AppCacheInfo mock_manifest_3; | 
|  | mock_manifest_1.manifest_url = kOriginURL.Resolve("manifest1"); | 
|  | mock_manifest_2.manifest_url = kOriginURL.Resolve("manifest2"); | 
|  | mock_manifest_3.manifest_url = kOriginURL.Resolve("manifest3"); | 
|  | AppCacheInfoVector info_vector; | 
|  | info_vector.push_back(mock_manifest_1); | 
|  | info_vector.push_back(mock_manifest_2); | 
|  | info_vector.push_back(mock_manifest_3); | 
|  | info->infos_by_origin[kOrigin] = info_vector; | 
|  | mock_storage()->SimulateGetAllInfo(info.get()); | 
|  | service_->DeleteAppCachesForOrigin(kOrigin, deletion_callback()); | 
|  | EXPECT_EQ(0, delete_completion_count_); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | EXPECT_EQ(1, delete_completion_count_); | 
|  | EXPECT_EQ(net::OK, delete_result_); | 
|  | delete_completion_count_ = 0; | 
|  |  | 
|  | // Should fail if storage fails to delete. | 
|  | info->infos_by_origin[kOrigin] = info_vector; | 
|  | mock_storage()->SimulateGetAllInfo(info.get()); | 
|  | mock_storage()->SimulateMakeGroupObsoleteFailure(); | 
|  | service_->DeleteAppCachesForOrigin(kOrigin, deletion_callback()); | 
|  | EXPECT_EQ(0, delete_completion_count_); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | EXPECT_EQ(1, delete_completion_count_); | 
|  | EXPECT_EQ(net::ERR_FAILED, delete_result_); | 
|  | delete_completion_count_ = 0; | 
|  |  | 
|  | // Should complete with abort error if the service is deleted | 
|  | // prior to a delete completion. | 
|  | service_->DeleteAppCachesForOrigin(kOrigin, deletion_callback()); | 
|  | EXPECT_EQ(0, delete_completion_count_); | 
|  | service_.reset();  // kill it | 
|  | EXPECT_EQ(1, delete_completion_count_); | 
|  | EXPECT_EQ(net::ERR_ABORTED, delete_result_); | 
|  | delete_completion_count_ = 0; | 
|  |  | 
|  | // Let any tasks lingering from the sudden deletion run and verify | 
|  | // no other completion calls occur. | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | EXPECT_EQ(0, delete_completion_count_); | 
|  | } | 
|  |  | 
|  | TEST_F(AppCacheServiceImplTest, CheckAppCacheResponse) { | 
|  | // Check a non-existing manifest. | 
|  | EXPECT_FALSE(IsGroupStored(kManifestUrl)); | 
|  | service_->CheckAppCacheResponse(kManifestUrl, 1, 1); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | EXPECT_EQ(0, CountPendingHelpers()); | 
|  | EXPECT_FALSE(IsGroupStored(kManifestUrl)); | 
|  | ResetStorage(); | 
|  |  | 
|  | // Check a response that looks good. | 
|  | // Nothing should be deleted. | 
|  | SetupMockGroup(); | 
|  | EXPECT_TRUE(IsGroupStored(kManifestUrl)); | 
|  | SetupMockReader(true, true, true); | 
|  | service_->CheckAppCacheResponse(kManifestUrl, kMockCacheId, kMockResponseId); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | EXPECT_EQ(0, CountPendingHelpers()); | 
|  | EXPECT_TRUE(IsGroupStored(kManifestUrl)); | 
|  | ResetStorage(); | 
|  |  | 
|  | // Check a response for which there is no cache entry. | 
|  | // The group should get deleted. | 
|  | SetupMockGroup(); | 
|  | service_->CheckAppCacheResponse(kManifestUrl, kMockCacheId, | 
|  | kMissingResponseId); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | EXPECT_EQ(0, CountPendingHelpers()); | 
|  | EXPECT_FALSE(IsGroupStored(kManifestUrl)); | 
|  | ResetStorage(); | 
|  |  | 
|  | // Check a response for which there is no manifest entry in a newer version | 
|  | // of the cache. Nothing should get deleted in this case. | 
|  | SetupMockGroup(); | 
|  | service_->CheckAppCacheResponse(kManifestUrl, kMissingCacheId, | 
|  | kMissingResponseId); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | EXPECT_EQ(0, CountPendingHelpers()); | 
|  | EXPECT_TRUE(IsGroupStored(kManifestUrl)); | 
|  | ResetStorage(); | 
|  |  | 
|  | // Check a response with bad headers. | 
|  | SetupMockGroup(); | 
|  | service_->CheckAppCacheResponse(kManifestUrl, kMockCacheId, kMockResponseId); | 
|  | SetupMockReader(false, true, true); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | EXPECT_EQ(0, CountPendingHelpers()); | 
|  | EXPECT_FALSE(IsGroupStored(kManifestUrl)); | 
|  | ResetStorage(); | 
|  |  | 
|  | // Check a response with bad data. | 
|  | SetupMockGroup(); | 
|  | service_->CheckAppCacheResponse(kManifestUrl, kMockCacheId, kMockResponseId); | 
|  | SetupMockReader(true, false, true); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | EXPECT_EQ(0, CountPendingHelpers()); | 
|  | EXPECT_FALSE(IsGroupStored(kManifestUrl)); | 
|  | ResetStorage(); | 
|  |  | 
|  | // Check a response with truncated data. | 
|  | SetupMockGroup(); | 
|  | service_->CheckAppCacheResponse(kManifestUrl, kMockCacheId, kMockResponseId); | 
|  | SetupMockReader(true, true, false); | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | EXPECT_EQ(0, CountPendingHelpers()); | 
|  | EXPECT_FALSE(IsGroupStored(kManifestUrl)); | 
|  | ResetStorage(); | 
|  |  | 
|  | service_.reset();  // Clean up. | 
|  | base::RunLoop().RunUntilIdle(); | 
|  | } | 
|  |  | 
|  | // Just tests the backoff scheduling function, not the actual reinit function. | 
|  | TEST_F(AppCacheServiceImplTest, ScheduleReinitialize) { | 
|  | const base::TimeDelta kNoDelay; | 
|  | const base::TimeDelta kOneSecond(base::TimeDelta::FromSeconds(1)); | 
|  | const base::TimeDelta k30Seconds(base::TimeDelta::FromSeconds(30)); | 
|  | const base::TimeDelta kOneHour(base::TimeDelta::FromHours(1)); | 
|  |  | 
|  | // Do things get initialized as expected? | 
|  | std::unique_ptr<AppCacheServiceImpl> service( | 
|  | new AppCacheServiceImpl(nullptr)); | 
|  | EXPECT_TRUE(service->last_reinit_time_.is_null()); | 
|  | EXPECT_FALSE(service->reinit_timer_.IsRunning()); | 
|  | EXPECT_EQ(kNoDelay, service->next_reinit_delay_); | 
|  |  | 
|  | // Do we see artifacts of the timer pending and such? | 
|  | service->ScheduleReinitialize(); | 
|  | EXPECT_TRUE(service->reinit_timer_.IsRunning()); | 
|  | EXPECT_EQ(kNoDelay, service->reinit_timer_.GetCurrentDelay()); | 
|  | EXPECT_EQ(k30Seconds, service->next_reinit_delay_); | 
|  |  | 
|  | // Nothing should change if already scheduled | 
|  | service->ScheduleReinitialize(); | 
|  | EXPECT_TRUE(service->reinit_timer_.IsRunning()); | 
|  | EXPECT_EQ(kNoDelay, service->reinit_timer_.GetCurrentDelay()); | 
|  | EXPECT_EQ(k30Seconds, service->next_reinit_delay_); | 
|  |  | 
|  | // Does the delay increase as expected? | 
|  | service->reinit_timer_.Stop(); | 
|  | service->last_reinit_time_ = base::Time::Now() - kOneSecond; | 
|  | service->ScheduleReinitialize(); | 
|  | EXPECT_TRUE(service->reinit_timer_.IsRunning()); | 
|  | EXPECT_EQ(k30Seconds, service->reinit_timer_.GetCurrentDelay()); | 
|  | EXPECT_EQ(k30Seconds + k30Seconds, service->next_reinit_delay_); | 
|  |  | 
|  | // Does the delay reset as expected? | 
|  | service->reinit_timer_.Stop(); | 
|  | service->last_reinit_time_ = base::Time::Now() - | 
|  | base::TimeDelta::FromHours(2); | 
|  | service->ScheduleReinitialize(); | 
|  | EXPECT_TRUE(service->reinit_timer_.IsRunning()); | 
|  | EXPECT_EQ(kNoDelay, service->reinit_timer_.GetCurrentDelay()); | 
|  | EXPECT_EQ(k30Seconds, service->next_reinit_delay_); | 
|  |  | 
|  | // Does the delay max out as expected? | 
|  | service->reinit_timer_.Stop(); | 
|  | service->last_reinit_time_ = base::Time::Now() - kOneSecond; | 
|  | service->next_reinit_delay_ = kOneHour; | 
|  | service->ScheduleReinitialize(); | 
|  | EXPECT_TRUE(service->reinit_timer_.IsRunning()); | 
|  | EXPECT_EQ(kOneHour, service->reinit_timer_.GetCurrentDelay()); | 
|  | EXPECT_EQ(kOneHour, service->next_reinit_delay_); | 
|  |  | 
|  | // Fine to delete while pending. | 
|  | service.reset(nullptr); | 
|  | } | 
|  |  | 
|  |  | 
|  |  | 
|  | }  // namespace content |