| // 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 "net/url_request/sdch_dictionary_fetcher.h" |
| |
| #include <algorithm> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/callback.h" |
| #include "base/logging.h" |
| #include "base/macros.h" |
| #include "base/run_loop.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "net/base/load_flags.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/sdch_manager.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/url_request/url_request_filter.h" |
| #include "net/url_request/url_request_interceptor.h" |
| #include "net/url_request/url_request_job.h" |
| #include "net/url_request/url_request_redirect_job.h" |
| #include "net/url_request/url_request_test_util.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace net { |
| |
| namespace { |
| |
| const char kSampleBufferContext[] = "This is a sample buffer."; |
| const char kTestDomain1[] = "top.domain.test"; |
| const char kTestDomain2[] = "top2.domain.test"; |
| |
| // A URLRequestJob that returns a fixed response body, based on the URL, with |
| // the specified HttpResponseInfo. Can also be made to return an error after the |
| // response body has been read. |
| class URLRequestSpecifiedResponseJob : public URLRequestJob { |
| public: |
| // Called on destruction with load flags used for this request. |
| typedef base::Callback<void(int)> DestructionCallback; |
| |
| URLRequestSpecifiedResponseJob( |
| URLRequest* request, |
| NetworkDelegate* network_delegate, |
| const HttpResponseInfo& response_info_to_return, |
| const DestructionCallback& destruction_callback) |
| : URLRequestJob(request, network_delegate), |
| response_info_to_return_(response_info_to_return), |
| last_load_flags_seen_(request->load_flags()), |
| destruction_callback_(destruction_callback), |
| bytes_read_(0), |
| final_read_result_(OK), |
| weak_factory_(this) { |
| DCHECK(!destruction_callback.is_null()); |
| } |
| |
| ~URLRequestSpecifiedResponseJob() override { |
| destruction_callback_.Run(last_load_flags_seen_); |
| } |
| |
| // Sets the result of the final read, after the entire body has been read. |
| // Defaults to OK. |
| void set_final_read_result(Error final_read_result) { |
| final_read_result_ = final_read_result; |
| } |
| |
| // URLRequestJob implementation: |
| void Start() override { |
| base::ThreadTaskRunnerHandle::Get()->PostTask( |
| FROM_HERE, base::Bind(&URLRequestSpecifiedResponseJob::StartAsync, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| int ReadRawData(IOBuffer* buf, int buf_size) override { |
| std::string response = ExpectedResponseForURL(request_->url()); |
| response = response.substr(bytes_read_); |
| size_t bytes_to_copy = |
| std::min(static_cast<size_t>(buf_size), response.size()); |
| if (bytes_to_copy == 0) |
| return final_read_result_; |
| memcpy(buf->data(), response.c_str(), bytes_to_copy); |
| bytes_read_ += bytes_to_copy; |
| return bytes_to_copy; |
| } |
| |
| static std::string ExpectedResponseForURL(const GURL& url) { |
| return base::StringPrintf("Response for %s\n%s\nEnd Response for %s\n", |
| url.spec().c_str(), |
| kSampleBufferContext, |
| url.spec().c_str()); |
| } |
| |
| void GetResponseInfo(HttpResponseInfo* info) override { |
| *info = response_info_to_return_; |
| } |
| |
| private: |
| void StartAsync() { NotifyHeadersComplete(); } |
| |
| const HttpResponseInfo response_info_to_return_; |
| int last_load_flags_seen_; |
| const DestructionCallback destruction_callback_; |
| |
| int bytes_read_; |
| Error final_read_result_; |
| |
| base::WeakPtrFactory<URLRequestSpecifiedResponseJob> weak_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(URLRequestSpecifiedResponseJob); |
| }; |
| |
| // Wrap URLRequestRedirectJob in a destruction callback. |
| class TestURLRequestRedirectJob : public URLRequestRedirectJob { |
| public: |
| TestURLRequestRedirectJob(URLRequest* request, |
| NetworkDelegate* network_delegate, |
| const GURL& redirect_destination, |
| ResponseCode response_code, |
| const std::string& redirect_reason, |
| base::Closure destruction_callback) |
| : URLRequestRedirectJob(request, |
| network_delegate, |
| redirect_destination, |
| response_code, |
| redirect_reason), |
| destruction_callback_(destruction_callback) {} |
| ~TestURLRequestRedirectJob() override { destruction_callback_.Run(); } |
| |
| private: |
| const base::Closure destruction_callback_; |
| }; |
| |
| const char kRedirectPath[] = "/redirect/"; |
| const char kBodyErrorPath[] = "/body_error/"; |
| |
| class SDCHTestRequestInterceptor : public URLRequestInterceptor { |
| public: |
| // A callback to be called whenever a URLRequestJob child of this |
| // interceptor is created or destroyed. The first argument will be the |
| // change in number of jobs (i.e. +1 for created, -1 for destroyed). |
| // The second argument will be undefined if the job is being created |
| // or a redirect job is being destroyed, and (for non-redirect job |
| // destruction) will contain the load flags passed to the request the |
| // job was created for. |
| typedef base::Callback<void(int outstanding_job_delta, |
| int destruction_load_flags)> LifecycleCallback; |
| |
| // |*info| will be returned from all child URLRequestSpecifiedResponseJobs. |
| // Note that: a) this pointer is shared with the caller, and the caller must |
| // guarantee that |*info| outlives the SDCHTestRequestInterceptor, and |
| // b) |*info| is mutable, and changes to should propagate to |
| // URLRequestSpecifiedResponseJobs created after any change. |
| SDCHTestRequestInterceptor(HttpResponseInfo* http_response_info, |
| const LifecycleCallback& lifecycle_callback) |
| : http_response_info_(http_response_info), |
| lifecycle_callback_(lifecycle_callback) { |
| DCHECK(!lifecycle_callback_.is_null()); |
| } |
| ~SDCHTestRequestInterceptor() override {} |
| |
| URLRequestJob* MaybeInterceptRequest( |
| URLRequest* request, |
| NetworkDelegate* network_delegate) const override { |
| lifecycle_callback_.Run(1, 0); |
| |
| std::string path = request->url().path(); |
| if (base::StartsWith(path, kRedirectPath, base::CompareCase::SENSITIVE)) { |
| return new TestURLRequestRedirectJob( |
| request, network_delegate, GURL(path.substr(strlen(kRedirectPath))), |
| URLRequestRedirectJob::REDIRECT_307_TEMPORARY_REDIRECT, "testing", |
| base::Bind(lifecycle_callback_, -1, 0)); |
| } |
| |
| std::unique_ptr<URLRequestSpecifiedResponseJob> job( |
| new URLRequestSpecifiedResponseJob( |
| request, network_delegate, *http_response_info_, |
| base::Bind(lifecycle_callback_, -1))); |
| if (base::StartsWith(path, kBodyErrorPath, base::CompareCase::SENSITIVE)) |
| job->set_final_read_result(net::ERR_FAILED); |
| return job.release(); |
| } |
| |
| // The caller must ensure that both |*http_response_info| and the |
| // callback remain valid for the lifetime of the |
| // SDCHTestRequestInterceptor (i.e. until Unregister() is called). |
| static void RegisterWithFilter(HttpResponseInfo* http_response_info, |
| const LifecycleCallback& lifecycle_callback) { |
| URLRequestFilter::GetInstance()->AddHostnameInterceptor( |
| "http", kTestDomain1, |
| std::unique_ptr<URLRequestInterceptor>(new SDCHTestRequestInterceptor( |
| http_response_info, lifecycle_callback))); |
| |
| URLRequestFilter::GetInstance()->AddHostnameInterceptor( |
| "https", kTestDomain1, |
| std::unique_ptr<URLRequestInterceptor>(new SDCHTestRequestInterceptor( |
| http_response_info, lifecycle_callback))); |
| |
| URLRequestFilter::GetInstance()->AddHostnameInterceptor( |
| "http", kTestDomain2, |
| std::unique_ptr<URLRequestInterceptor>(new SDCHTestRequestInterceptor( |
| http_response_info, lifecycle_callback))); |
| |
| URLRequestFilter::GetInstance()->AddHostnameInterceptor( |
| "https", kTestDomain2, |
| std::unique_ptr<URLRequestInterceptor>(new SDCHTestRequestInterceptor( |
| http_response_info, lifecycle_callback))); |
| } |
| |
| static void Unregister() { |
| URLRequestFilter::GetInstance()->RemoveHostnameHandler("http", |
| kTestDomain1); |
| URLRequestFilter::GetInstance()->RemoveHostnameHandler("https", |
| kTestDomain1); |
| URLRequestFilter::GetInstance()->RemoveHostnameHandler("http", |
| kTestDomain2); |
| URLRequestFilter::GetInstance()->RemoveHostnameHandler("https", |
| kTestDomain2); |
| } |
| |
| private: |
| HttpResponseInfo* http_response_info_; |
| LifecycleCallback lifecycle_callback_; |
| DISALLOW_COPY_AND_ASSIGN(SDCHTestRequestInterceptor); |
| }; |
| |
| // Local test infrastructure |
| // * URLRequestSpecifiedResponseJob: A URLRequestJob that returns |
| // a different but derivable response for each URL (used for all |
| // url requests in this file). This class is initialized with |
| // the HttpResponseInfo to return (if any), as well as a callback |
| // that is called when the class is destroyed. That callback |
| // takes as arguemnt the load flags used for the request the |
| // job was created for. |
| // * SDCHTestRequestInterceptor: This class is a |
| // URLRequestInterceptor that generates either the class above or an |
| // instance of URLRequestRedirectJob (if the first component of the path |
| // is "redirect"). It is constructed |
| // with a pointer to the (mutable) resposne info that should be |
| // returned from constructed URLRequestSpecifiedResponseJobs, as well as |
| // a callback that is run when URLRequestSpecifiedResponseJobs are |
| // created or destroyed. |
| // * SdchDictionaryFetcherTest: This class registers the above interceptor, |
| // tracks the number of jobs requested and the subset of those |
| // that are still outstanding. It exports an interface to wait until there |
| // are no jobs outstanding. It shares an HttpResponseInfo structure |
| // with the SDCHTestRequestInterceptor to control the response |
| // information returned by the jbos. |
| // The standard pattern for tests is to schedule a dictionary fetch, wait |
| // for no jobs outstanding, then test that the fetch results are as expected. |
| |
| class SdchDictionaryFetcherTest : public ::testing::Test { |
| public: |
| struct DictionaryAdditions { |
| DictionaryAdditions(const std::string& dictionary_text, |
| const GURL& dictionary_url) |
| : dictionary_text(dictionary_text), dictionary_url(dictionary_url) {} |
| |
| std::string dictionary_text; |
| GURL dictionary_url; |
| }; |
| |
| SdchDictionaryFetcherTest() |
| : jobs_requested_(0), |
| jobs_outstanding_(0), |
| last_load_flags_seen_(LOAD_NORMAL), |
| context_(new TestURLRequestContext), |
| fetcher_(new SdchDictionaryFetcher(context_.get())), |
| factory_(this) { |
| response_info_to_return_.request_time = base::Time::Now(); |
| response_info_to_return_.response_time = base::Time::Now(); |
| SDCHTestRequestInterceptor::RegisterWithFilter( |
| &response_info_to_return_, |
| base::Bind(&SdchDictionaryFetcherTest::OnNumberJobsChanged, |
| factory_.GetWeakPtr())); |
| } |
| |
| ~SdchDictionaryFetcherTest() override { |
| SDCHTestRequestInterceptor::Unregister(); |
| } |
| |
| void OnDictionaryFetched(const std::string& dictionary_text, |
| const GURL& dictionary_url, |
| const BoundNetLog& net_log, |
| bool was_from_cache) { |
| dictionary_additions_.push_back( |
| DictionaryAdditions(dictionary_text, dictionary_url)); |
| } |
| |
| // Return (in |*out|) all dictionary additions since the last time |
| // this function was called. |
| void GetDictionaryAdditions(std::vector<DictionaryAdditions>* out) { |
| out->swap(dictionary_additions_); |
| dictionary_additions_.clear(); |
| } |
| |
| SdchDictionaryFetcher* fetcher() { return fetcher_.get(); } |
| |
| // May not be called outside the SetUp()/TearDown() interval. |
| int jobs_requested() const { return jobs_requested_; } |
| |
| GURL PathToGurl(const char* path) const { |
| std::string gurl_string("http://"); |
| gurl_string += kTestDomain1; |
| gurl_string += "/"; |
| gurl_string += path; |
| return GURL(gurl_string); |
| } |
| |
| // Block until there are no outstanding URLRequestSpecifiedResponseJobs. |
| void WaitForNoJobs() { |
| // A job may be started after the previous one was destroyed, with a brief |
| // period of 0 jobs in between, so may have to start the run loop multiple |
| // times. |
| while (jobs_outstanding_ != 0) { |
| run_loop_.reset(new base::RunLoop); |
| run_loop_->Run(); |
| run_loop_.reset(); |
| } |
| } |
| |
| HttpResponseInfo* response_info_to_return() { |
| return &response_info_to_return_; |
| } |
| |
| int last_load_flags_seen() const { return last_load_flags_seen_; } |
| |
| const SdchDictionaryFetcher::OnDictionaryFetchedCallback |
| GetDefaultCallback() { |
| return base::Bind(&SdchDictionaryFetcherTest::OnDictionaryFetched, |
| base::Unretained(this)); |
| } |
| |
| private: |
| void OnNumberJobsChanged(int outstanding_jobs_delta, int load_flags) { |
| DCHECK_NE(0, outstanding_jobs_delta); |
| if (outstanding_jobs_delta > 0) |
| jobs_requested_ += outstanding_jobs_delta; |
| else |
| last_load_flags_seen_ = load_flags; |
| jobs_outstanding_ += outstanding_jobs_delta; |
| if (jobs_outstanding_ == 0 && run_loop_) |
| run_loop_->Quit(); |
| } |
| |
| int jobs_requested_; |
| int jobs_outstanding_; |
| |
| // Last load flags seen by the interceptor installed in |
| // SdchDictionaryFetcherTest(). These are available to test bodies and |
| // currently used for ensuring that certain loads are marked only-from-cache. |
| int last_load_flags_seen_; |
| |
| std::unique_ptr<base::RunLoop> run_loop_; |
| std::unique_ptr<TestURLRequestContext> context_; |
| std::unique_ptr<SdchDictionaryFetcher> fetcher_; |
| std::vector<DictionaryAdditions> dictionary_additions_; |
| |
| // The request_time and response_time fields are filled in by the constructor |
| // for SdchDictionaryFetcherTest. Tests can fill the other fields of this |
| // member in to alter the HttpResponseInfo returned by the fetcher's |
| // URLRequestJob. |
| HttpResponseInfo response_info_to_return_; |
| |
| base::WeakPtrFactory<SdchDictionaryFetcherTest> factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(SdchDictionaryFetcherTest); |
| }; |
| |
| // Schedule a fetch and make sure it happens. |
| TEST_F(SdchDictionaryFetcherTest, Basic) { |
| GURL dictionary_url(PathToGurl("dictionary")); |
| fetcher()->Schedule(dictionary_url, GetDefaultCallback()); |
| WaitForNoJobs(); |
| |
| EXPECT_EQ(1, jobs_requested()); |
| std::vector<DictionaryAdditions> additions; |
| GetDictionaryAdditions(&additions); |
| ASSERT_EQ(1u, additions.size()); |
| EXPECT_EQ( |
| URLRequestSpecifiedResponseJob::ExpectedResponseForURL(dictionary_url), |
| additions[0].dictionary_text); |
| EXPECT_FALSE(last_load_flags_seen() & LOAD_ONLY_FROM_CACHE); |
| } |
| |
| // Multiple fetches of the same URL should result in only one request. |
| TEST_F(SdchDictionaryFetcherTest, Multiple) { |
| GURL dictionary_url(PathToGurl("dictionary")); |
| EXPECT_TRUE(fetcher()->Schedule(dictionary_url, GetDefaultCallback())); |
| EXPECT_FALSE(fetcher()->Schedule(dictionary_url, GetDefaultCallback())); |
| EXPECT_FALSE(fetcher()->Schedule(dictionary_url, GetDefaultCallback())); |
| WaitForNoJobs(); |
| |
| EXPECT_EQ(1, jobs_requested()); |
| std::vector<DictionaryAdditions> additions; |
| GetDictionaryAdditions(&additions); |
| ASSERT_EQ(1u, additions.size()); |
| EXPECT_EQ( |
| URLRequestSpecifiedResponseJob::ExpectedResponseForURL(dictionary_url), |
| additions[0].dictionary_text); |
| } |
| |
| // A cancel should result in no actual requests being generated. |
| TEST_F(SdchDictionaryFetcherTest, Cancel) { |
| GURL dictionary_url_1(PathToGurl("dictionary_1")); |
| GURL dictionary_url_2(PathToGurl("dictionary_2")); |
| GURL dictionary_url_3(PathToGurl("dictionary_3")); |
| |
| fetcher()->Schedule(dictionary_url_1, GetDefaultCallback()); |
| fetcher()->Schedule(dictionary_url_2, GetDefaultCallback()); |
| fetcher()->Schedule(dictionary_url_3, GetDefaultCallback()); |
| fetcher()->Cancel(); |
| WaitForNoJobs(); |
| |
| // Synchronous execution may have resulted in a single job being scheduled. |
| EXPECT_GE(1, jobs_requested()); |
| } |
| |
| // Attempt to confuse the fetcher loop processing by scheduling a |
| // dictionary addition while another fetch is in process. |
| TEST_F(SdchDictionaryFetcherTest, LoopRace) { |
| GURL dictionary0_url(PathToGurl("dictionary0")); |
| GURL dictionary1_url(PathToGurl("dictionary1")); |
| fetcher()->Schedule(dictionary0_url, GetDefaultCallback()); |
| fetcher()->Schedule(dictionary1_url, GetDefaultCallback()); |
| WaitForNoJobs(); |
| |
| ASSERT_EQ(2, jobs_requested()); |
| std::vector<DictionaryAdditions> additions; |
| GetDictionaryAdditions(&additions); |
| ASSERT_EQ(2u, additions.size()); |
| EXPECT_EQ( |
| URLRequestSpecifiedResponseJob::ExpectedResponseForURL(dictionary0_url), |
| additions[0].dictionary_text); |
| EXPECT_EQ( |
| URLRequestSpecifiedResponseJob::ExpectedResponseForURL(dictionary1_url), |
| additions[1].dictionary_text); |
| } |
| |
| TEST_F(SdchDictionaryFetcherTest, ScheduleReloadLoadFlags) { |
| GURL dictionary_url(PathToGurl("dictionary")); |
| fetcher()->ScheduleReload(dictionary_url, GetDefaultCallback()); |
| |
| WaitForNoJobs(); |
| EXPECT_EQ(1, jobs_requested()); |
| std::vector<DictionaryAdditions> additions; |
| GetDictionaryAdditions(&additions); |
| ASSERT_EQ(1u, additions.size()); |
| EXPECT_EQ( |
| URLRequestSpecifiedResponseJob::ExpectedResponseForURL(dictionary_url), |
| additions[0].dictionary_text); |
| EXPECT_TRUE(last_load_flags_seen() & LOAD_ONLY_FROM_CACHE); |
| } |
| |
| TEST_F(SdchDictionaryFetcherTest, ScheduleReloadFresh) { |
| std::string raw_headers = "\0"; |
| response_info_to_return()->headers = new HttpResponseHeaders( |
| HttpUtil::AssembleRawHeaders(raw_headers.data(), raw_headers.size())); |
| response_info_to_return()->headers->AddHeader("Cache-Control: max-age=1000"); |
| |
| GURL dictionary_url(PathToGurl("dictionary")); |
| fetcher()->ScheduleReload(dictionary_url, GetDefaultCallback()); |
| |
| WaitForNoJobs(); |
| EXPECT_EQ(1, jobs_requested()); |
| std::vector<DictionaryAdditions> additions; |
| GetDictionaryAdditions(&additions); |
| ASSERT_EQ(1u, additions.size()); |
| EXPECT_EQ( |
| URLRequestSpecifiedResponseJob::ExpectedResponseForURL(dictionary_url), |
| additions[0].dictionary_text); |
| EXPECT_TRUE(last_load_flags_seen() & LOAD_ONLY_FROM_CACHE); |
| } |
| |
| TEST_F(SdchDictionaryFetcherTest, ScheduleReloadStale) { |
| response_info_to_return()->headers = new HttpResponseHeaders(""); |
| response_info_to_return()->headers->AddHeader("Cache-Control: no-cache"); |
| |
| GURL dictionary_url(PathToGurl("dictionary")); |
| fetcher()->ScheduleReload(dictionary_url, GetDefaultCallback()); |
| |
| WaitForNoJobs(); |
| EXPECT_EQ(1, jobs_requested()); |
| std::vector<DictionaryAdditions> additions; |
| GetDictionaryAdditions(&additions); |
| EXPECT_EQ(0u, additions.size()); |
| EXPECT_TRUE(last_load_flags_seen() & LOAD_ONLY_FROM_CACHE); |
| } |
| |
| TEST_F(SdchDictionaryFetcherTest, ScheduleReloadThenLoad) { |
| GURL dictionary_url(PathToGurl("dictionary")); |
| EXPECT_TRUE(fetcher()->ScheduleReload(dictionary_url, GetDefaultCallback())); |
| EXPECT_TRUE(fetcher()->Schedule(dictionary_url, GetDefaultCallback())); |
| |
| WaitForNoJobs(); |
| EXPECT_EQ(2, jobs_requested()); |
| } |
| |
| TEST_F(SdchDictionaryFetcherTest, ScheduleLoadThenReload) { |
| GURL dictionary_url(PathToGurl("dictionary")); |
| EXPECT_TRUE(fetcher()->Schedule(dictionary_url, GetDefaultCallback())); |
| EXPECT_FALSE(fetcher()->ScheduleReload(dictionary_url, GetDefaultCallback())); |
| |
| WaitForNoJobs(); |
| EXPECT_EQ(1, jobs_requested()); |
| } |
| |
| TEST_F(SdchDictionaryFetcherTest, CancelAllowsFutureFetches) { |
| GURL dictionary_url(PathToGurl("dictionary")); |
| EXPECT_TRUE(fetcher()->Schedule(dictionary_url, GetDefaultCallback())); |
| EXPECT_FALSE(fetcher()->Schedule(dictionary_url, GetDefaultCallback())); |
| |
| WaitForNoJobs(); |
| EXPECT_EQ(1, jobs_requested()); |
| |
| fetcher()->Cancel(); |
| WaitForNoJobs(); |
| EXPECT_TRUE(fetcher()->Schedule(dictionary_url, GetDefaultCallback())); |
| |
| WaitForNoJobs(); |
| EXPECT_EQ(2, jobs_requested()); |
| } |
| |
| TEST_F(SdchDictionaryFetcherTest, Redirect) { |
| GURL dictionary_url(PathToGurl("dictionary")); |
| GURL local_redirect_url(dictionary_url.GetWithEmptyPath().spec() + |
| "redirect/" + dictionary_url.spec()); |
| EXPECT_TRUE(fetcher()->Schedule(local_redirect_url, GetDefaultCallback())); |
| WaitForNoJobs(); |
| |
| // The redirect should have been rejected with no dictionary added. |
| EXPECT_EQ(1, jobs_requested()); |
| std::vector<DictionaryAdditions> additions; |
| GetDictionaryAdditions(&additions); |
| EXPECT_EQ(0u, additions.size()); |
| |
| // Simple SDCH dictionary fetch test, to make sure the fetcher was left |
| // in reasonable shape by the above. |
| |
| GURL dictionary2_url(PathToGurl("dictionary2")); |
| fetcher()->Schedule(dictionary2_url, GetDefaultCallback()); |
| WaitForNoJobs(); |
| |
| EXPECT_EQ(2, jobs_requested()); |
| GetDictionaryAdditions(&additions); |
| ASSERT_EQ(1u, additions.size()); |
| EXPECT_EQ( |
| URLRequestSpecifiedResponseJob::ExpectedResponseForURL(dictionary2_url), |
| additions[0].dictionary_text); |
| EXPECT_FALSE(last_load_flags_seen() & LOAD_ONLY_FROM_CACHE); |
| } |
| |
| // Check the case of two requests for different URLs, where the first request |
| // fails after receiving body data. |
| TEST_F(SdchDictionaryFetcherTest, TwoDictionariesFirstFails) { |
| GURL dictionary_with_error_url(PathToGurl("body_error/")); |
| GURL dictionary_url(PathToGurl("dictionary")); |
| EXPECT_TRUE( |
| fetcher()->Schedule(dictionary_with_error_url, GetDefaultCallback())); |
| EXPECT_TRUE(fetcher()->Schedule(dictionary_url, GetDefaultCallback())); |
| WaitForNoJobs(); |
| |
| EXPECT_EQ(2, jobs_requested()); |
| std::vector<DictionaryAdditions> additions; |
| GetDictionaryAdditions(&additions); |
| // Should only have a dictionary for the successful request. |
| ASSERT_EQ(1u, additions.size()); |
| EXPECT_EQ( |
| URLRequestSpecifiedResponseJob::ExpectedResponseForURL(dictionary_url), |
| additions[0].dictionary_text); |
| } |
| |
| } // namespace |
| |
| } // namespace net |