// Copyright (c) 2012 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 "base/bind.h" #include "base/bind_helpers.h" #include "base/memory/scoped_ptr.h" #include "base/message_loop/message_loop.h" #include "net/url_request/url_request.h" #include "testing/gtest/include/gtest/gtest.h" #include "webkit/browser/appcache/appcache.h" #include "webkit/browser/appcache/appcache_backend_impl.h" #include "webkit/browser/appcache/appcache_group.h" #include "webkit/browser/appcache/appcache_host.h" #include "webkit/browser/appcache/mock_appcache_policy.h" #include "webkit/browser/appcache/mock_appcache_service.h" #include "webkit/browser/quota/quota_manager.h" namespace appcache { class AppCacheHostTest : public testing::Test { public: AppCacheHostTest() { get_status_callback_ = base::Bind(&AppCacheHostTest::GetStatusCallback, base::Unretained(this)); start_update_callback_ = base::Bind(&AppCacheHostTest::StartUpdateCallback, base::Unretained(this)); swap_cache_callback_ = base::Bind(&AppCacheHostTest::SwapCacheCallback, base::Unretained(this)); } class MockFrontend : public AppCacheFrontend { public: MockFrontend() : last_host_id_(-222), last_cache_id_(-222), last_status_(appcache::OBSOLETE), last_status_changed_(appcache::OBSOLETE), last_event_id_(appcache::OBSOLETE_EVENT), content_blocked_(false) { } virtual void OnCacheSelected( int host_id, const appcache::AppCacheInfo& info) OVERRIDE { last_host_id_ = host_id; last_cache_id_ = info.cache_id; last_status_ = info.status; } virtual void OnStatusChanged(const std::vector<int>& host_ids, appcache::Status status) OVERRIDE { last_status_changed_ = status; } virtual void OnEventRaised(const std::vector<int>& host_ids, appcache::EventID event_id) OVERRIDE { last_event_id_ = event_id; } virtual void OnErrorEventRaised(const std::vector<int>& host_ids, const std::string& message) OVERRIDE { last_event_id_ = ERROR_EVENT; } virtual void OnProgressEventRaised(const std::vector<int>& host_ids, const GURL& url, int num_total, int num_complete) OVERRIDE { last_event_id_ = PROGRESS_EVENT; } virtual void OnLogMessage(int host_id, appcache::LogLevel log_level, const std::string& message) OVERRIDE { } virtual void OnContentBlocked(int host_id, const GURL& manifest_url) OVERRIDE { content_blocked_ = true; } int last_host_id_; int64 last_cache_id_; appcache::Status last_status_; appcache::Status last_status_changed_; appcache::EventID last_event_id_; bool content_blocked_; }; class MockQuotaManagerProxy : public quota::QuotaManagerProxy { public: MockQuotaManagerProxy() : QuotaManagerProxy(NULL, NULL) {} // Not needed for our tests. virtual void RegisterClient(quota::QuotaClient* client) OVERRIDE {} virtual void NotifyStorageAccessed(quota::QuotaClient::ID client_id, const GURL& origin, quota::StorageType type) OVERRIDE {} virtual void NotifyStorageModified(quota::QuotaClient::ID client_id, const GURL& origin, quota::StorageType type, int64 delta) OVERRIDE {} virtual void SetUsageCacheEnabled(quota::QuotaClient::ID client_id, const GURL& origin, quota::StorageType type, bool enabled) OVERRIDE {} virtual void GetUsageAndQuota( base::SequencedTaskRunner* original_task_runner, const GURL& origin, quota::StorageType type, const GetUsageAndQuotaCallback& callback) OVERRIDE {} virtual void NotifyOriginInUse(const GURL& origin) OVERRIDE { inuse_[origin] += 1; } virtual void NotifyOriginNoLongerInUse(const GURL& origin) OVERRIDE { inuse_[origin] -= 1; } int GetInUseCount(const GURL& origin) { return inuse_[origin]; } void reset() { inuse_.clear(); } // Map from origin to count of inuse notifications. std::map<GURL, int> inuse_; protected: virtual ~MockQuotaManagerProxy() {} }; void GetStatusCallback(Status status, void* param) { last_status_result_ = status; last_callback_param_ = param; } void StartUpdateCallback(bool result, void* param) { last_start_result_ = result; last_callback_param_ = param; } void SwapCacheCallback(bool result, void* param) { last_swap_result_ = result; last_callback_param_ = param; } base::MessageLoop message_loop_; // Mock classes for the 'host' to work with MockAppCacheService service_; MockFrontend mock_frontend_; // Mock callbacks we expect to receive from the 'host' appcache::GetStatusCallback get_status_callback_; appcache::StartUpdateCallback start_update_callback_; appcache::SwapCacheCallback swap_cache_callback_; Status last_status_result_; bool last_swap_result_; bool last_start_result_; void* last_callback_param_; }; TEST_F(AppCacheHostTest, Basic) { // Construct a host and test what state it appears to be in. AppCacheHost host(1, &mock_frontend_, &service_); EXPECT_EQ(1, host.host_id()); EXPECT_EQ(&service_, host.service()); EXPECT_EQ(&mock_frontend_, host.frontend()); EXPECT_EQ(NULL, host.associated_cache()); EXPECT_FALSE(host.is_selection_pending()); // See that the callbacks are delivered immediately // and respond as if there is no cache selected. last_status_result_ = OBSOLETE; host.GetStatusWithCallback(get_status_callback_, reinterpret_cast<void*>(1)); EXPECT_EQ(UNCACHED, last_status_result_); EXPECT_EQ(reinterpret_cast<void*>(1), last_callback_param_); last_start_result_ = true; host.StartUpdateWithCallback(start_update_callback_, reinterpret_cast<void*>(2)); EXPECT_FALSE(last_start_result_); EXPECT_EQ(reinterpret_cast<void*>(2), last_callback_param_); last_swap_result_ = true; host.SwapCacheWithCallback(swap_cache_callback_, reinterpret_cast<void*>(3)); EXPECT_FALSE(last_swap_result_); EXPECT_EQ(reinterpret_cast<void*>(3), last_callback_param_); } TEST_F(AppCacheHostTest, SelectNoCache) { scoped_refptr<MockQuotaManagerProxy> mock_quota_proxy( new MockQuotaManagerProxy); service_.set_quota_manager_proxy(mock_quota_proxy.get()); // Reset our mock frontend mock_frontend_.last_cache_id_ = -333; mock_frontend_.last_host_id_ = -333; mock_frontend_.last_status_ = OBSOLETE; const GURL kDocAndOriginUrl(GURL("http://whatever/").GetOrigin()); { AppCacheHost host(1, &mock_frontend_, &service_); host.SelectCache(kDocAndOriginUrl, kNoCacheId, GURL()); EXPECT_EQ(1, mock_quota_proxy->GetInUseCount(kDocAndOriginUrl)); // We should have received an OnCacheSelected msg EXPECT_EQ(1, mock_frontend_.last_host_id_); EXPECT_EQ(kNoCacheId, mock_frontend_.last_cache_id_); EXPECT_EQ(UNCACHED, mock_frontend_.last_status_); // Otherwise, see that it respond as if there is no cache selected. EXPECT_EQ(1, host.host_id()); EXPECT_EQ(&service_, host.service()); EXPECT_EQ(&mock_frontend_, host.frontend()); EXPECT_EQ(NULL, host.associated_cache()); EXPECT_FALSE(host.is_selection_pending()); EXPECT_TRUE(host.preferred_manifest_url().is_empty()); } EXPECT_EQ(0, mock_quota_proxy->GetInUseCount(kDocAndOriginUrl)); service_.set_quota_manager_proxy(NULL); } TEST_F(AppCacheHostTest, ForeignEntry) { // Reset our mock frontend mock_frontend_.last_cache_id_ = -333; mock_frontend_.last_host_id_ = -333; mock_frontend_.last_status_ = OBSOLETE; // Precondition, a cache with an entry that is not marked as foreign. const int kCacheId = 22; const GURL kDocumentURL("http://origin/document"); scoped_refptr<AppCache> cache = new AppCache(service_.storage(), kCacheId); cache->AddEntry(kDocumentURL, AppCacheEntry(AppCacheEntry::EXPLICIT)); AppCacheHost host(1, &mock_frontend_, &service_); host.MarkAsForeignEntry(kDocumentURL, kCacheId); // We should have received an OnCacheSelected msg for kNoCacheId. EXPECT_EQ(1, mock_frontend_.last_host_id_); EXPECT_EQ(kNoCacheId, mock_frontend_.last_cache_id_); EXPECT_EQ(UNCACHED, mock_frontend_.last_status_); // See that it respond as if there is no cache selected. EXPECT_EQ(1, host.host_id()); EXPECT_EQ(&service_, host.service()); EXPECT_EQ(&mock_frontend_, host.frontend()); EXPECT_EQ(NULL, host.associated_cache()); EXPECT_FALSE(host.is_selection_pending()); // See that the entry was marked as foreign. EXPECT_TRUE(cache->GetEntry(kDocumentURL)->IsForeign()); } TEST_F(AppCacheHostTest, ForeignFallbackEntry) { // Reset our mock frontend mock_frontend_.last_cache_id_ = -333; mock_frontend_.last_host_id_ = -333; mock_frontend_.last_status_ = OBSOLETE; // Precondition, a cache with a fallback entry that is not marked as foreign. const int kCacheId = 22; const GURL kFallbackURL("http://origin/fallback_resource"); scoped_refptr<AppCache> cache = new AppCache(service_.storage(), kCacheId); cache->AddEntry(kFallbackURL, AppCacheEntry(AppCacheEntry::FALLBACK)); AppCacheHost host(1, &mock_frontend_, &service_); host.NotifyMainResourceIsNamespaceEntry(kFallbackURL); host.MarkAsForeignEntry(GURL("http://origin/missing_document"), kCacheId); // We should have received an OnCacheSelected msg for kNoCacheId. EXPECT_EQ(1, mock_frontend_.last_host_id_); EXPECT_EQ(kNoCacheId, mock_frontend_.last_cache_id_); EXPECT_EQ(UNCACHED, mock_frontend_.last_status_); // See that the fallback entry was marked as foreign. EXPECT_TRUE(cache->GetEntry(kFallbackURL)->IsForeign()); } TEST_F(AppCacheHostTest, FailedCacheLoad) { // Reset our mock frontend mock_frontend_.last_cache_id_ = -333; mock_frontend_.last_host_id_ = -333; mock_frontend_.last_status_ = OBSOLETE; AppCacheHost host(1, &mock_frontend_, &service_); EXPECT_FALSE(host.is_selection_pending()); const int kMockCacheId = 333; // Put it in a state where we're waiting on a cache // load prior to finishing cache selection. host.pending_selected_cache_id_ = kMockCacheId; EXPECT_TRUE(host.is_selection_pending()); // The callback should not occur until we finish cache selection. last_status_result_ = OBSOLETE; last_callback_param_ = reinterpret_cast<void*>(-1); host.GetStatusWithCallback(get_status_callback_, reinterpret_cast<void*>(1)); EXPECT_EQ(OBSOLETE, last_status_result_); EXPECT_EQ(reinterpret_cast<void*>(-1), last_callback_param_); // Satisfy the load with NULL, a failure. host.OnCacheLoaded(NULL, kMockCacheId); // Cache selection should have finished EXPECT_FALSE(host.is_selection_pending()); EXPECT_EQ(1, mock_frontend_.last_host_id_); EXPECT_EQ(kNoCacheId, mock_frontend_.last_cache_id_); EXPECT_EQ(UNCACHED, mock_frontend_.last_status_); // Callback should have fired upon completing the cache load too. EXPECT_EQ(UNCACHED, last_status_result_); EXPECT_EQ(reinterpret_cast<void*>(1), last_callback_param_); } TEST_F(AppCacheHostTest, FailedGroupLoad) { AppCacheHost host(1, &mock_frontend_, &service_); const GURL kMockManifestUrl("http://foo.bar/baz"); // Put it in a state where we're waiting on a cache // load prior to finishing cache selection. host.pending_selected_manifest_url_ = kMockManifestUrl; EXPECT_TRUE(host.is_selection_pending()); // The callback should not occur until we finish cache selection. last_status_result_ = OBSOLETE; last_callback_param_ = reinterpret_cast<void*>(-1); host.GetStatusWithCallback(get_status_callback_, reinterpret_cast<void*>(1)); EXPECT_EQ(OBSOLETE, last_status_result_); EXPECT_EQ(reinterpret_cast<void*>(-1), last_callback_param_); // Satisfy the load will NULL, a failure. host.OnGroupLoaded(NULL, kMockManifestUrl); // Cache selection should have finished EXPECT_FALSE(host.is_selection_pending()); EXPECT_EQ(1, mock_frontend_.last_host_id_); EXPECT_EQ(kNoCacheId, mock_frontend_.last_cache_id_); EXPECT_EQ(UNCACHED, mock_frontend_.last_status_); // Callback should have fired upon completing the group load. EXPECT_EQ(UNCACHED, last_status_result_); EXPECT_EQ(reinterpret_cast<void*>(1), last_callback_param_); } TEST_F(AppCacheHostTest, SetSwappableCache) { AppCacheHost host(1, &mock_frontend_, &service_); host.SetSwappableCache(NULL); EXPECT_FALSE(host.swappable_cache_.get()); scoped_refptr<AppCacheGroup> group1(new AppCacheGroup( service_.storage(), GURL(), service_.storage()->NewGroupId())); host.SetSwappableCache(group1.get()); EXPECT_FALSE(host.swappable_cache_.get()); AppCache* cache1 = new AppCache(service_.storage(), 111); cache1->set_complete(true); group1->AddCache(cache1); host.SetSwappableCache(group1.get()); EXPECT_EQ(cache1, host.swappable_cache_.get()); mock_frontend_.last_host_id_ = -222; // to verify we received OnCacheSelected host.AssociateCompleteCache(cache1); EXPECT_FALSE(host.swappable_cache_.get()); // was same as associated cache EXPECT_EQ(appcache::IDLE, host.GetStatus()); // verify OnCacheSelected was called EXPECT_EQ(host.host_id(), mock_frontend_.last_host_id_); EXPECT_EQ(cache1->cache_id(), mock_frontend_.last_cache_id_); EXPECT_EQ(appcache::IDLE, mock_frontend_.last_status_); AppCache* cache2 = new AppCache(service_.storage(), 222); cache2->set_complete(true); group1->AddCache(cache2); EXPECT_EQ(cache2, host.swappable_cache_.get()); // updated to newest scoped_refptr<AppCacheGroup> group2( new AppCacheGroup(service_.storage(), GURL("http://foo.com"), service_.storage()->NewGroupId())); AppCache* cache3 = new AppCache(service_.storage(), 333); cache3->set_complete(true); group2->AddCache(cache3); AppCache* cache4 = new AppCache(service_.storage(), 444); cache4->set_complete(true); group2->AddCache(cache4); EXPECT_EQ(cache2, host.swappable_cache_.get()); // unchanged host.AssociateCompleteCache(cache3); EXPECT_EQ(cache4, host.swappable_cache_.get()); // newest cache in group2 EXPECT_FALSE(group1->HasCache()); // both caches in group1 have refcount 0 host.AssociateNoCache(GURL()); EXPECT_FALSE(host.swappable_cache_.get()); EXPECT_FALSE(group2->HasCache()); // both caches in group2 have refcount 0 // Host adds reference to newest cache when an update is complete. AppCache* cache5 = new AppCache(service_.storage(), 555); cache5->set_complete(true); group2->AddCache(cache5); host.group_being_updated_ = group2; host.OnUpdateComplete(group2.get()); EXPECT_FALSE(host.group_being_updated_.get()); EXPECT_EQ(cache5, host.swappable_cache_.get()); group2->RemoveCache(cache5); EXPECT_FALSE(group2->HasCache()); host.group_being_updated_ = group2; host.OnUpdateComplete(group2.get()); EXPECT_FALSE(host.group_being_updated_.get()); EXPECT_FALSE(host.swappable_cache_.get()); // group2 had no newest cache } TEST_F(AppCacheHostTest, ForDedicatedWorker) { const int kMockProcessId = 1; const int kParentHostId = 1; const int kWorkerHostId = 2; AppCacheBackendImpl backend_impl; backend_impl.Initialize(&service_, &mock_frontend_, kMockProcessId); backend_impl.RegisterHost(kParentHostId); backend_impl.RegisterHost(kWorkerHostId); AppCacheHost* parent_host = backend_impl.GetHost(kParentHostId); EXPECT_FALSE(parent_host->is_for_dedicated_worker()); AppCacheHost* worker_host = backend_impl.GetHost(kWorkerHostId); worker_host->SelectCacheForWorker(kParentHostId, kMockProcessId); EXPECT_TRUE(worker_host->is_for_dedicated_worker()); EXPECT_EQ(parent_host, worker_host->GetParentAppCacheHost()); // We should have received an OnCacheSelected msg for the worker_host. // The host for workers always indicates 'no cache selected' regardless // of its parent's state. This is OK because the worker cannot access // the scriptable interface, the only function available is resource // loading (see appcache_request_handler_unittests those tests). EXPECT_EQ(kWorkerHostId, mock_frontend_.last_host_id_); EXPECT_EQ(kNoCacheId, mock_frontend_.last_cache_id_); EXPECT_EQ(UNCACHED, mock_frontend_.last_status_); // Simulate the parent being torn down. backend_impl.UnregisterHost(kParentHostId); parent_host = NULL; EXPECT_EQ(NULL, backend_impl.GetHost(kParentHostId)); EXPECT_EQ(NULL, worker_host->GetParentAppCacheHost()); } TEST_F(AppCacheHostTest, SelectCacheAllowed) { scoped_refptr<MockQuotaManagerProxy> mock_quota_proxy( new MockQuotaManagerProxy); MockAppCachePolicy mock_appcache_policy; mock_appcache_policy.can_create_return_value_ = true; service_.set_quota_manager_proxy(mock_quota_proxy.get()); service_.set_appcache_policy(&mock_appcache_policy); // Reset our mock frontend mock_frontend_.last_cache_id_ = -333; mock_frontend_.last_host_id_ = -333; mock_frontend_.last_status_ = OBSOLETE; mock_frontend_.last_event_id_ = OBSOLETE_EVENT; mock_frontend_.content_blocked_ = false; const GURL kDocAndOriginUrl(GURL("http://whatever/").GetOrigin()); const GURL kManifestUrl(GURL("http://whatever/cache.manifest")); { AppCacheHost host(1, &mock_frontend_, &service_); host.first_party_url_ = kDocAndOriginUrl; host.SelectCache(kDocAndOriginUrl, kNoCacheId, kManifestUrl); EXPECT_EQ(1, mock_quota_proxy->GetInUseCount(kDocAndOriginUrl)); // MockAppCacheService::LoadOrCreateGroup is asynchronous, so we shouldn't // have received an OnCacheSelected msg yet. EXPECT_EQ(-333, mock_frontend_.last_host_id_); EXPECT_EQ(-333, mock_frontend_.last_cache_id_); EXPECT_EQ(OBSOLETE, mock_frontend_.last_status_); // No error events either EXPECT_EQ(OBSOLETE_EVENT, mock_frontend_.last_event_id_); EXPECT_FALSE(mock_frontend_.content_blocked_); EXPECT_TRUE(host.is_selection_pending()); } EXPECT_EQ(0, mock_quota_proxy->GetInUseCount(kDocAndOriginUrl)); service_.set_quota_manager_proxy(NULL); } TEST_F(AppCacheHostTest, SelectCacheBlocked) { scoped_refptr<MockQuotaManagerProxy> mock_quota_proxy( new MockQuotaManagerProxy); MockAppCachePolicy mock_appcache_policy; mock_appcache_policy.can_create_return_value_ = false; service_.set_quota_manager_proxy(mock_quota_proxy.get()); service_.set_appcache_policy(&mock_appcache_policy); // Reset our mock frontend mock_frontend_.last_cache_id_ = -333; mock_frontend_.last_host_id_ = -333; mock_frontend_.last_status_ = OBSOLETE; mock_frontend_.last_event_id_ = OBSOLETE_EVENT; mock_frontend_.content_blocked_ = false; const GURL kDocAndOriginUrl(GURL("http://whatever/").GetOrigin()); const GURL kManifestUrl(GURL("http://whatever/cache.manifest")); { AppCacheHost host(1, &mock_frontend_, &service_); host.first_party_url_ = kDocAndOriginUrl; host.SelectCache(kDocAndOriginUrl, kNoCacheId, kManifestUrl); EXPECT_EQ(1, mock_quota_proxy->GetInUseCount(kDocAndOriginUrl)); // We should have received an OnCacheSelected msg EXPECT_EQ(1, mock_frontend_.last_host_id_); EXPECT_EQ(kNoCacheId, mock_frontend_.last_cache_id_); EXPECT_EQ(UNCACHED, mock_frontend_.last_status_); // Also, an error event was raised EXPECT_EQ(ERROR_EVENT, mock_frontend_.last_event_id_); EXPECT_TRUE(mock_frontend_.content_blocked_); // Otherwise, see that it respond as if there is no cache selected. EXPECT_EQ(1, host.host_id()); EXPECT_EQ(&service_, host.service()); EXPECT_EQ(&mock_frontend_, host.frontend()); EXPECT_EQ(NULL, host.associated_cache()); EXPECT_FALSE(host.is_selection_pending()); EXPECT_TRUE(host.preferred_manifest_url().is_empty()); } EXPECT_EQ(0, mock_quota_proxy->GetInUseCount(kDocAndOriginUrl)); service_.set_quota_manager_proxy(NULL); } } // namespace appcache