// 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 "base/bind.h"
#include "base/bind_helpers.h"
#include "base/memory/scoped_ptr.h"
#include "base/message_loop/message_loop.h"
#include "content/browser/appcache/appcache.h"
#include "content/browser/appcache/appcache_backend_impl.h"
#include "content/browser/appcache/appcache_group.h"
#include "content/browser/appcache/appcache_host.h"
#include "content/browser/appcache/mock_appcache_policy.h"
#include "content/browser/appcache/mock_appcache_service.h"
#include "net/url_request/url_request.h"
#include "storage/browser/quota/quota_manager.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace content {
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_STATUS_OBSOLETE),
last_status_changed_(APPCACHE_STATUS_OBSOLETE),
last_event_id_(APPCACHE_OBSOLETE_EVENT),
content_blocked_(false) {
}
virtual void OnCacheSelected(
int host_id, const 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,
AppCacheStatus status) OVERRIDE {
last_status_changed_ = status;
}
virtual void OnEventRaised(const std::vector<int>& host_ids,
AppCacheEventID event_id) OVERRIDE {
last_event_id_ = event_id;
}
virtual void OnErrorEventRaised(
const std::vector<int>& host_ids,
const AppCacheErrorDetails& details) OVERRIDE {
last_event_id_ = APPCACHE_ERROR_EVENT;
}
virtual void OnProgressEventRaised(const std::vector<int>& host_ids,
const GURL& url,
int num_total,
int num_complete) OVERRIDE {
last_event_id_ = APPCACHE_PROGRESS_EVENT;
}
virtual void OnLogMessage(int host_id,
AppCacheLogLevel 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_;
AppCacheStatus last_status_;
AppCacheStatus last_status_changed_;
AppCacheEventID last_event_id_;
bool content_blocked_;
};
class MockQuotaManagerProxy : public storage::QuotaManagerProxy {
public:
MockQuotaManagerProxy() : QuotaManagerProxy(NULL, NULL) {}
// Not needed for our tests.
virtual void RegisterClient(storage::QuotaClient* client) OVERRIDE {}
virtual void NotifyStorageAccessed(storage::QuotaClient::ID client_id,
const GURL& origin,
storage::StorageType type) OVERRIDE {}
virtual void NotifyStorageModified(storage::QuotaClient::ID client_id,
const GURL& origin,
storage::StorageType type,
int64 delta) OVERRIDE {}
virtual void SetUsageCacheEnabled(storage::QuotaClient::ID client_id,
const GURL& origin,
storage::StorageType type,
bool enabled) OVERRIDE {}
virtual void GetUsageAndQuota(
base::SequencedTaskRunner* original_task_runner,
const GURL& origin,
storage::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(AppCacheStatus 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'
content::GetStatusCallback get_status_callback_;
content::StartUpdateCallback start_update_callback_;
content::SwapCacheCallback swap_cache_callback_;
AppCacheStatus 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_ = APPCACHE_STATUS_OBSOLETE;
host.GetStatusWithCallback(get_status_callback_, reinterpret_cast<void*>(1));
EXPECT_EQ(APPCACHE_STATUS_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_ = APPCACHE_STATUS_OBSOLETE;
const GURL kDocAndOriginUrl(GURL("http://whatever/").GetOrigin());
{
AppCacheHost host(1, &mock_frontend_, &service_);
host.SelectCache(kDocAndOriginUrl, kAppCacheNoCacheId, 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(kAppCacheNoCacheId, mock_frontend_.last_cache_id_);
EXPECT_EQ(APPCACHE_STATUS_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_ = APPCACHE_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 kAppCacheNoCacheId.
EXPECT_EQ(1, mock_frontend_.last_host_id_);
EXPECT_EQ(kAppCacheNoCacheId, mock_frontend_.last_cache_id_);
EXPECT_EQ(APPCACHE_STATUS_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_ = APPCACHE_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 kAppCacheNoCacheId.
EXPECT_EQ(1, mock_frontend_.last_host_id_);
EXPECT_EQ(kAppCacheNoCacheId, mock_frontend_.last_cache_id_);
EXPECT_EQ(APPCACHE_STATUS_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_ = APPCACHE_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_ = APPCACHE_STATUS_OBSOLETE;
last_callback_param_ = reinterpret_cast<void*>(-1);
host.GetStatusWithCallback(get_status_callback_, reinterpret_cast<void*>(1));
EXPECT_EQ(APPCACHE_STATUS_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(kAppCacheNoCacheId, mock_frontend_.last_cache_id_);
EXPECT_EQ(APPCACHE_STATUS_UNCACHED, mock_frontend_.last_status_);
// Callback should have fired upon completing the cache load too.
EXPECT_EQ(APPCACHE_STATUS_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_ = APPCACHE_STATUS_OBSOLETE;
last_callback_param_ = reinterpret_cast<void*>(-1);
host.GetStatusWithCallback(get_status_callback_, reinterpret_cast<void*>(1));
EXPECT_EQ(APPCACHE_STATUS_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(kAppCacheNoCacheId, mock_frontend_.last_cache_id_);
EXPECT_EQ(APPCACHE_STATUS_UNCACHED, mock_frontend_.last_status_);
// Callback should have fired upon completing the group load.
EXPECT_EQ(APPCACHE_STATUS_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_STATUS_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_STATUS_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(kAppCacheNoCacheId, mock_frontend_.last_cache_id_);
EXPECT_EQ(APPCACHE_STATUS_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_ = APPCACHE_STATUS_OBSOLETE;
mock_frontend_.last_event_id_ = APPCACHE_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, kAppCacheNoCacheId, 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(APPCACHE_STATUS_OBSOLETE, mock_frontend_.last_status_);
// No error events either
EXPECT_EQ(APPCACHE_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_ = APPCACHE_STATUS_OBSOLETE;
mock_frontend_.last_event_id_ = APPCACHE_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, kAppCacheNoCacheId, 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(kAppCacheNoCacheId, mock_frontend_.last_cache_id_);
EXPECT_EQ(APPCACHE_STATUS_UNCACHED, mock_frontend_.last_status_);
// Also, an error event was raised
EXPECT_EQ(APPCACHE_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 content