1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 #include <algorithm> 6 7 #include "base/pickle.h" 8 #include "base/time.h" 9 #include "chrome/browser/profiles/profile.h" 10 #include "chrome/browser/safe_browsing/malware_details.h" 11 #include "chrome/browser/safe_browsing/report.pb.h" 12 #include "chrome/common/render_messages.h" 13 #include "chrome/common/safe_browsing/safebrowsing_messages.h" 14 #include "chrome/test/test_url_request_context_getter.h" 15 #include "chrome/test/testing_profile.h" 16 #include "content/browser/browser_thread.h" 17 #include "content/browser/renderer_host/test_render_view_host.h" 18 #include "content/browser/tab_contents/navigation_entry.h" 19 #include "content/browser/tab_contents/test_tab_contents.h" 20 #include "net/base/io_buffer.h" 21 #include "net/base/test_completion_callback.h" 22 #include "net/disk_cache/disk_cache.h" 23 #include "net/http/http_cache.h" 24 #include "net/http/http_response_headers.h" 25 #include "net/http/http_response_info.h" 26 #include "net/http/http_util.h" 27 #include "net/url_request/url_request_context.h" 28 #include "net/url_request/url_request_context_getter.h" 29 30 static const char* kOriginalLandingURL = "http://www.originallandingpage.com/"; 31 static const char* kHttpsURL = "https://www.url.com/"; 32 static const char* kDOMChildURL = "http://www.domparent.com/"; 33 static const char* kDOMParentURL = "http://www.domchild.com/"; 34 static const char* kFirstRedirectURL = "http://redirectone.com/"; 35 static const char* kSecondRedirectURL = "http://redirecttwo.com/"; 36 37 static const char* kMalwareURL = "http://www.malware.com/"; 38 static const char* kMalwareHeaders = 39 "HTTP/1.1 200 OK\n" 40 "Content-Type: image/jpeg\n"; 41 static const char* kMalwareData = "exploit();"; 42 43 static const char* kLandingURL = "http://www.landingpage.com/"; 44 static const char* kLandingHeaders = 45 "HTTP/1.1 200 OK\n" 46 "Content-Type: text/html\n" 47 "Content-Length: 1024\n" 48 "Set-Cookie: tastycookie\n"; // This header is stripped. 49 static const char* kLandingData = "<iframe src='http://www.malware.com'>"; 50 51 using safe_browsing::ClientMalwareReportRequest; 52 53 namespace { 54 55 void WriteHeaders(disk_cache::Entry* entry, const std::string headers) { 56 net::HttpResponseInfo responseinfo; 57 std::string raw_headers = net::HttpUtil::AssembleRawHeaders( 58 headers.c_str(), headers.size()); 59 responseinfo.headers = new net::HttpResponseHeaders(raw_headers); 60 61 Pickle pickle; 62 responseinfo.Persist(&pickle, false, false); 63 64 scoped_refptr<net::WrappedIOBuffer> buf(new net::WrappedIOBuffer( 65 reinterpret_cast<const char*>(pickle.data()))); 66 int len = static_cast<int>(pickle.size()); 67 68 TestCompletionCallback cb; 69 int rv = entry->WriteData(0, 0, buf, len, &cb, true); 70 ASSERT_EQ(len, cb.GetResult(rv)); 71 } 72 73 void WriteData(disk_cache::Entry* entry, const std::string data) { 74 if (data.empty()) 75 return; 76 77 int len = data.length(); 78 scoped_refptr<net::IOBuffer> buf(new net::IOBuffer(len)); 79 memcpy(buf->data(), data.data(), data.length()); 80 81 TestCompletionCallback cb; 82 int rv = entry->WriteData(1, 0, buf, len, &cb, true); 83 ASSERT_EQ(len, cb.GetResult(rv)); 84 } 85 86 void WriteToEntry(disk_cache::Backend* cache, const std::string key, 87 const std::string headers, const std::string data) { 88 TestCompletionCallback cb; 89 disk_cache::Entry* entry; 90 int rv = cache->CreateEntry(key, &entry, &cb); 91 rv = cb.GetResult(rv); 92 if (rv != net::OK) { 93 rv = cache->OpenEntry(key, &entry, &cb); 94 ASSERT_EQ(net::OK, cb.GetResult(rv)); 95 } 96 97 WriteHeaders(entry, headers); 98 WriteData(entry, data); 99 100 entry->Close(); 101 } 102 103 void FillCache(net::URLRequestContext* context) { 104 TestCompletionCallback cb; 105 disk_cache::Backend* cache; 106 int rv = 107 context->http_transaction_factory()->GetCache()->GetBackend(&cache, &cb); 108 ASSERT_EQ(net::OK, cb.GetResult(rv)); 109 110 std::string empty; 111 WriteToEntry(cache, kMalwareURL, kMalwareHeaders, kMalwareData); 112 WriteToEntry(cache, kLandingURL, kLandingHeaders, kLandingData); 113 } 114 115 void QuitUIMessageLoop() { 116 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); 117 BrowserThread::PostTask(BrowserThread::UI, 118 FROM_HERE, 119 new MessageLoop::QuitTask()); 120 } 121 122 // Lets us provide a MockURLRequestContext with an HTTP Cache we pre-populate. 123 // Also exposes the constructor. 124 class MalwareDetailsWrap : public MalwareDetails { 125 public: 126 MalwareDetailsWrap(SafeBrowsingService* sb_service, 127 TabContents* tab_contents, 128 const SafeBrowsingService::UnsafeResource& unsafe_resource, 129 net::URLRequestContextGetter* request_context_getter) 130 : MalwareDetails(sb_service, tab_contents, unsafe_resource) { 131 request_context_getter_ = request_context_getter; 132 } 133 134 virtual ~MalwareDetailsWrap() {} 135 }; 136 137 class MockSafeBrowsingService : public SafeBrowsingService { 138 public: 139 MockSafeBrowsingService() {} 140 virtual ~MockSafeBrowsingService() {} 141 142 // When the MalwareDetails is done, this is called. 143 virtual void SendSerializedMalwareDetails(const std::string& serialized) { 144 DVLOG(1) << "SendSerializedMalwareDetails"; 145 // Notify WaitForSerializedReport. 146 BrowserThread::PostTask(BrowserThread::IO, 147 FROM_HERE, 148 NewRunnableFunction(&QuitUIMessageLoop)); 149 serialized_ = serialized; 150 } 151 152 const std::string& GetSerialized() { 153 return serialized_; 154 } 155 156 private: 157 std::string serialized_; 158 DISALLOW_COPY_AND_ASSIGN(MockSafeBrowsingService); 159 }; 160 161 } // namespace. 162 163 class MalwareDetailsTest : public RenderViewHostTestHarness { 164 public: 165 MalwareDetailsTest() 166 : ui_thread_(BrowserThread::UI, &message_loop_), 167 io_thread_(BrowserThread::IO), 168 sb_service_(new MockSafeBrowsingService()) { 169 } 170 171 virtual void SetUp() { 172 RenderViewHostTestHarness::SetUp(); 173 // request_context_getter_ = new TestURLRequestContextGetter(); 174 175 // The URLFetcher checks that the messageloop type is IO. 176 ASSERT_TRUE(io_thread_.StartWithOptions( 177 base::Thread::Options(MessageLoop::TYPE_IO, 0))); 178 } 179 180 virtual void TearDown() { 181 io_thread_.Stop(); 182 RenderViewHostTestHarness::TearDown(); 183 } 184 185 static bool ResourceLessThan( 186 const ClientMalwareReportRequest::Resource* lhs, 187 const ClientMalwareReportRequest::Resource* rhs) { 188 return lhs->id() < rhs->id(); 189 } 190 191 std::string WaitForSerializedReport(MalwareDetails* report) { 192 BrowserThread::PostTask( 193 BrowserThread::IO, 194 FROM_HERE, 195 NewRunnableMethod( 196 report, &MalwareDetails::FinishCollection)); 197 // Wait for the callback (SendSerializedMalwareDetails). 198 DVLOG(1) << "Waiting for SendSerializedMalwareDetails"; 199 MessageLoop::current()->Run(); 200 return sb_service_->GetSerialized(); 201 } 202 203 protected: 204 void InitResource(SafeBrowsingService::UnsafeResource* resource, 205 ResourceType::Type resource_type, 206 const GURL& url) { 207 resource->client = NULL; 208 resource->url = url; 209 resource->resource_type = resource_type; 210 resource->threat_type = SafeBrowsingService::URL_MALWARE; 211 resource->render_process_host_id = contents()->GetRenderProcessHost()->id(); 212 resource->render_view_id = contents()->render_view_host()->routing_id(); 213 } 214 215 void VerifyResults(const ClientMalwareReportRequest& report_pb, 216 const ClientMalwareReportRequest& expected_pb) { 217 EXPECT_EQ(expected_pb.malware_url(), report_pb.malware_url()); 218 EXPECT_EQ(expected_pb.page_url(), report_pb.page_url()); 219 EXPECT_EQ(expected_pb.referrer_url(), report_pb.referrer_url()); 220 221 ASSERT_EQ(expected_pb.resources_size(), report_pb.resources_size()); 222 // Sort the resources, to make the test deterministic 223 std::vector<const ClientMalwareReportRequest::Resource*> resources; 224 for (int i = 0; i < report_pb.resources_size(); ++i) { 225 const ClientMalwareReportRequest::Resource& resource = 226 report_pb.resources(i); 227 resources.push_back(&resource); 228 } 229 std::sort(resources.begin(), resources.end(), 230 &MalwareDetailsTest::ResourceLessThan); 231 232 std::vector<const ClientMalwareReportRequest::Resource*> expected; 233 for (int i = 0; i < report_pb.resources_size(); ++i) { 234 const ClientMalwareReportRequest::Resource& resource = 235 expected_pb.resources(i); 236 expected.push_back(&resource); 237 } 238 std::sort(expected.begin(), expected.end(), 239 &MalwareDetailsTest::ResourceLessThan); 240 241 for (uint32 i = 0; i < expected.size(); ++i) { 242 VerifyResource(resources[i], expected[i]); 243 } 244 245 EXPECT_EQ(expected_pb.complete(), report_pb.complete()); 246 } 247 248 void VerifyResource(const ClientMalwareReportRequest::Resource* resource, 249 const ClientMalwareReportRequest::Resource* expected) { 250 EXPECT_EQ(expected->id(), resource->id()); 251 EXPECT_EQ(expected->url(), resource->url()); 252 EXPECT_EQ(expected->parent_id(), resource->parent_id()); 253 ASSERT_EQ(expected->child_ids_size(), resource->child_ids_size()); 254 for (int i = 0; i < expected->child_ids_size(); i++) { 255 EXPECT_EQ(expected->child_ids(i), resource->child_ids(i)); 256 } 257 258 // Verify HTTP Responses 259 if (expected->has_response()) { 260 ASSERT_TRUE(resource->has_response()); 261 EXPECT_EQ(expected->response().firstline().code(), 262 resource->response().firstline().code()); 263 264 ASSERT_EQ(expected->response().headers_size(), 265 resource->response().headers_size()); 266 for (int i = 0; i < expected->response().headers_size(); ++i) { 267 EXPECT_EQ(expected->response().headers(i).name(), 268 resource->response().headers(i).name()); 269 EXPECT_EQ(expected->response().headers(i).value(), 270 resource->response().headers(i).value()); 271 } 272 273 EXPECT_EQ(expected->response().body(), resource->response().body()); 274 EXPECT_EQ(expected->response().bodylength(), 275 resource->response().bodylength()); 276 EXPECT_EQ(expected->response().bodydigest(), 277 resource->response().bodydigest()); 278 } 279 } 280 281 BrowserThread ui_thread_; 282 BrowserThread io_thread_; 283 scoped_refptr<MockSafeBrowsingService> sb_service_; 284 }; 285 286 // Tests creating a simple malware report. 287 TEST_F(MalwareDetailsTest, MalwareSubResource) { 288 // Start a load. 289 controller().LoadURL(GURL(kLandingURL), GURL(), PageTransition::TYPED); 290 291 SafeBrowsingService::UnsafeResource resource; 292 InitResource(&resource, ResourceType::SUB_RESOURCE, GURL(kMalwareURL)); 293 294 scoped_refptr<MalwareDetailsWrap> report = new MalwareDetailsWrap( 295 sb_service_, contents(), resource, NULL); 296 297 std::string serialized = WaitForSerializedReport(report); 298 299 ClientMalwareReportRequest actual; 300 actual.ParseFromString(serialized); 301 302 ClientMalwareReportRequest expected; 303 expected.set_malware_url(kMalwareURL); 304 expected.set_page_url(kLandingURL); 305 expected.set_referrer_url(""); 306 307 ClientMalwareReportRequest::Resource* pb_resource = expected.add_resources(); 308 pb_resource->set_id(0); 309 pb_resource->set_url(kLandingURL); 310 pb_resource = expected.add_resources(); 311 pb_resource->set_id(1); 312 pb_resource->set_url(kMalwareURL); 313 314 VerifyResults(actual, expected); 315 } 316 317 // Tests creating a simple malware report where the subresource has a 318 // different original_url. 319 TEST_F(MalwareDetailsTest, MalwareSubResourceWithOriginalUrl) { 320 controller().LoadURL(GURL(kLandingURL), GURL(), PageTransition::TYPED); 321 322 SafeBrowsingService::UnsafeResource resource; 323 InitResource(&resource, ResourceType::SUB_RESOURCE, GURL(kMalwareURL)); 324 resource.original_url = GURL(kOriginalLandingURL); 325 326 scoped_refptr<MalwareDetailsWrap> report = new MalwareDetailsWrap( 327 sb_service_.get(), contents(), resource, NULL); 328 329 std::string serialized = WaitForSerializedReport(report); 330 331 ClientMalwareReportRequest actual; 332 actual.ParseFromString(serialized); 333 334 ClientMalwareReportRequest expected; 335 expected.set_malware_url(kMalwareURL); 336 expected.set_page_url(kLandingURL); 337 expected.set_referrer_url(""); 338 339 ClientMalwareReportRequest::Resource* pb_resource = expected.add_resources(); 340 pb_resource->set_id(0); 341 pb_resource->set_url(kLandingURL); 342 343 pb_resource = expected.add_resources(); 344 pb_resource->set_id(1); 345 pb_resource->set_url(kOriginalLandingURL); 346 347 pb_resource = expected.add_resources(); 348 pb_resource->set_id(2); 349 pb_resource->set_url(kMalwareURL); 350 // The Resource for kMmalwareUrl should have the Resource for 351 // kOriginalLandingURL (with id 1) as parent. 352 pb_resource->set_parent_id(1); 353 354 VerifyResults(actual, expected); 355 } 356 357 // Tests creating a malware report with data from the renderer. 358 TEST_F(MalwareDetailsTest, MalwareDOMDetails) { 359 controller().LoadURL(GURL(kLandingURL), GURL(), PageTransition::TYPED); 360 361 SafeBrowsingService::UnsafeResource resource; 362 InitResource(&resource, ResourceType::SUB_RESOURCE, GURL(kMalwareURL)); 363 364 scoped_refptr<MalwareDetailsWrap> report = new MalwareDetailsWrap( 365 sb_service_.get(), contents(), resource, NULL); 366 367 // Send a message from the DOM, with 2 nodes, a parent and a child. 368 std::vector<SafeBrowsingHostMsg_MalwareDOMDetails_Node> params; 369 SafeBrowsingHostMsg_MalwareDOMDetails_Node child_node; 370 child_node.url = GURL(kDOMChildURL); 371 child_node.tag_name = "iframe"; 372 child_node.parent = GURL(kDOMParentURL); 373 params.push_back(child_node); 374 SafeBrowsingHostMsg_MalwareDOMDetails_Node parent_node; 375 parent_node.url = GURL(kDOMParentURL); 376 parent_node.children.push_back(GURL(kDOMChildURL)); 377 params.push_back(parent_node); 378 report->OnReceivedMalwareDOMDetails(params); 379 380 MessageLoop::current()->RunAllPending(); 381 382 std::string serialized = WaitForSerializedReport(report); 383 ClientMalwareReportRequest actual; 384 actual.ParseFromString(serialized); 385 386 ClientMalwareReportRequest expected; 387 expected.set_malware_url(kMalwareURL); 388 expected.set_page_url(kLandingURL); 389 expected.set_referrer_url(""); 390 391 ClientMalwareReportRequest::Resource* pb_resource = expected.add_resources(); 392 pb_resource->set_id(0); 393 pb_resource->set_url(kLandingURL); 394 395 pb_resource = expected.add_resources(); 396 pb_resource->set_id(1); 397 pb_resource->set_url(kMalwareURL); 398 399 pb_resource = expected.add_resources(); 400 pb_resource->set_id(2); 401 pb_resource->set_url(kDOMChildURL); 402 pb_resource->set_parent_id(3); 403 404 pb_resource = expected.add_resources(); 405 pb_resource->set_id(3); 406 pb_resource->set_url(kDOMParentURL); 407 pb_resource->add_child_ids(2); 408 expected.set_complete(false); // Since the cache was missing. 409 410 VerifyResults(actual, expected); 411 } 412 413 // Verify that https:// urls are dropped. 414 TEST_F(MalwareDetailsTest, NotPublicUrl) { 415 controller().LoadURL(GURL(kHttpsURL), GURL(), PageTransition::TYPED); 416 SafeBrowsingService::UnsafeResource resource; 417 InitResource(&resource, ResourceType::SUB_RESOURCE, GURL(kMalwareURL)); 418 scoped_refptr<MalwareDetailsWrap> report = new MalwareDetailsWrap( 419 sb_service_.get(), contents(), resource, NULL); 420 421 std::string serialized = WaitForSerializedReport(report); 422 ClientMalwareReportRequest actual; 423 actual.ParseFromString(serialized); 424 425 ClientMalwareReportRequest expected; 426 expected.set_malware_url(kMalwareURL); // No page_url 427 expected.set_referrer_url(""); 428 429 ClientMalwareReportRequest::Resource* pb_resource = expected.add_resources(); 430 pb_resource->set_url(kMalwareURL); // Only one resource 431 432 VerifyResults(actual, expected); 433 } 434 435 // Tests creating a malware report where there are redirect urls to an unsafe 436 // resource url 437 TEST_F(MalwareDetailsTest, MalwareWithRedirectUrl) { 438 controller().LoadURL(GURL(kLandingURL), GURL(), PageTransition::TYPED); 439 440 SafeBrowsingService::UnsafeResource resource; 441 InitResource(&resource, ResourceType::SUB_RESOURCE, GURL(kMalwareURL)); 442 resource.original_url = GURL(kOriginalLandingURL); 443 444 // add some redirect urls 445 resource.redirect_urls.push_back(GURL(kFirstRedirectURL)); 446 resource.redirect_urls.push_back(GURL(kSecondRedirectURL)); 447 resource.redirect_urls.push_back(GURL(kMalwareURL)); 448 449 scoped_refptr<MalwareDetailsWrap> report = new MalwareDetailsWrap( 450 sb_service_.get(), contents(), resource, NULL); 451 452 std::string serialized = WaitForSerializedReport(report); 453 ClientMalwareReportRequest actual; 454 actual.ParseFromString(serialized); 455 456 ClientMalwareReportRequest expected; 457 expected.set_malware_url(kMalwareURL); 458 expected.set_page_url(kLandingURL); 459 expected.set_referrer_url(""); 460 461 ClientMalwareReportRequest::Resource* pb_resource = expected.add_resources(); 462 pb_resource->set_id(0); 463 pb_resource->set_url(kLandingURL); 464 465 pb_resource = expected.add_resources(); 466 pb_resource->set_id(1); 467 pb_resource->set_url(kOriginalLandingURL); 468 469 pb_resource = expected.add_resources(); 470 pb_resource->set_id(2); 471 pb_resource->set_url(kMalwareURL); 472 pb_resource->set_parent_id(4); 473 474 pb_resource = expected.add_resources(); 475 pb_resource->set_id(3); 476 pb_resource->set_url(kFirstRedirectURL); 477 pb_resource->set_parent_id(1); 478 479 pb_resource = expected.add_resources(); 480 pb_resource->set_id(4); 481 pb_resource->set_url(kSecondRedirectURL); 482 pb_resource->set_parent_id(3); 483 484 VerifyResults(actual, expected); 485 } 486 487 // Tests the interaction with the HTTP cache. 488 TEST_F(MalwareDetailsTest, HTTPCache) { 489 controller().LoadURL(GURL(kLandingURL), GURL(), PageTransition::TYPED); 490 491 SafeBrowsingService::UnsafeResource resource; 492 InitResource(&resource, ResourceType::SUB_RESOURCE, GURL(kMalwareURL)); 493 494 profile()->CreateRequestContext(); 495 scoped_refptr<MalwareDetailsWrap> report = new MalwareDetailsWrap( 496 sb_service_.get(), contents(), resource 497 , profile()->GetRequestContext()); 498 499 FillCache(profile()->GetRequestContext()->GetURLRequestContext()); 500 501 // The cache collection starts after the IPC from the DOM is fired. 502 std::vector<SafeBrowsingHostMsg_MalwareDOMDetails_Node> params; 503 report->OnReceivedMalwareDOMDetails(params); 504 505 // Let the cache callbacks complete 506 MessageLoop::current()->RunAllPending(); 507 508 DVLOG(1) << "Getting serialized report"; 509 std::string serialized = WaitForSerializedReport(report); 510 ClientMalwareReportRequest actual; 511 actual.ParseFromString(serialized); 512 513 ClientMalwareReportRequest expected; 514 expected.set_malware_url(kMalwareURL); 515 expected.set_page_url(kLandingURL); 516 expected.set_referrer_url(""); 517 518 ClientMalwareReportRequest::Resource* pb_resource = expected.add_resources(); 519 pb_resource->set_id(0); 520 pb_resource->set_url(kLandingURL); 521 safe_browsing::ClientMalwareReportRequest::HTTPResponse* pb_response = 522 pb_resource->mutable_response(); 523 pb_response->mutable_firstline()->set_code(200); 524 safe_browsing::ClientMalwareReportRequest::HTTPHeader* pb_header = 525 pb_response->add_headers(); 526 pb_header->set_name("Content-Type"); 527 pb_header->set_value("text/html"); 528 pb_header = pb_response->add_headers(); 529 pb_header->set_name("Content-Length"); 530 pb_header->set_value("1024"); 531 pb_header = pb_response->add_headers(); 532 pb_header->set_name("Set-Cookie"); 533 pb_header->set_value(""); // The cookie is dropped. 534 pb_response->set_body(kLandingData); 535 pb_response->set_bodylength(37); 536 pb_response->set_bodydigest("9ca97475598a79bc1e8fc9bd6c72cd35"); 537 538 pb_resource = expected.add_resources(); 539 pb_resource->set_id(1); 540 pb_resource->set_url(kMalwareURL); 541 pb_response = pb_resource->mutable_response(); 542 pb_response->mutable_firstline()->set_code(200); 543 pb_header = pb_response->add_headers(); 544 pb_header->set_name("Content-Type"); 545 pb_header->set_value("image/jpeg"); 546 pb_response->set_body(kMalwareData); 547 pb_response->set_bodylength(10); 548 pb_response->set_bodydigest("581373551c43d4cf33bfb3b26838ff95"); 549 expected.set_complete(true); 550 551 VerifyResults(actual, expected); 552 } 553 554 // Tests the interaction with the HTTP cache (where the cache is empty). 555 TEST_F(MalwareDetailsTest, HTTPCacheNoEntries) { 556 controller().LoadURL(GURL(kLandingURL), GURL(), PageTransition::TYPED); 557 558 SafeBrowsingService::UnsafeResource resource; 559 InitResource(&resource, ResourceType::SUB_RESOURCE, GURL(kMalwareURL)); 560 561 profile()->CreateRequestContext(); 562 scoped_refptr<MalwareDetailsWrap> report = new MalwareDetailsWrap( 563 sb_service_.get(), contents(), resource, 564 profile()->GetRequestContext()); 565 566 // No call to FillCache 567 568 // The cache collection starts after the IPC from the DOM is fired. 569 std::vector<SafeBrowsingHostMsg_MalwareDOMDetails_Node> params; 570 report->OnReceivedMalwareDOMDetails(params); 571 572 // Let the cache callbacks complete 573 MessageLoop::current()->RunAllPending(); 574 575 DVLOG(1) << "Getting serialized report"; 576 std::string serialized = WaitForSerializedReport(report); 577 ClientMalwareReportRequest actual; 578 actual.ParseFromString(serialized); 579 580 ClientMalwareReportRequest expected; 581 expected.set_malware_url(kMalwareURL); 582 expected.set_page_url(kLandingURL); 583 expected.set_referrer_url(""); 584 585 ClientMalwareReportRequest::Resource* pb_resource = expected.add_resources(); 586 pb_resource->set_id(0); 587 pb_resource->set_url(kLandingURL); 588 pb_resource = expected.add_resources(); 589 pb_resource->set_id(1); 590 pb_resource->set_url(kMalwareURL); 591 expected.set_complete(true); 592 593 VerifyResults(actual, expected); 594 } 595