Componentize ShortcutsBackend
[chromium-blink-merge.git] / chrome / renderer / safe_browsing / phishing_dom_feature_extractor_browsertest.cc
blobc15219fcffa942f488a374037268bd1f46b5863f
1 // Copyright (c) 2012 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 // Note that although this is not a "browser" test, it runs as part of
6 // browser_tests. This is because WebKit does not work properly if it is
7 // shutdown and re-initialized. Since browser_tests runs each test in a
8 // new process, this avoids the problem.
10 #include "chrome/renderer/safe_browsing/phishing_dom_feature_extractor.h"
12 #include "base/bind.h"
13 #include "base/callback.h"
14 #include "base/command_line.h"
15 #include "base/compiler_specific.h"
16 #include "base/location.h"
17 #include "base/memory/weak_ptr.h"
18 #include "base/single_thread_task_runner.h"
19 #include "base/strings/string_number_conversions.h"
20 #include "base/thread_task_runner_handle.h"
21 #include "base/time/time.h"
22 #include "chrome/browser/ui/browser.h"
23 #include "chrome/browser/ui/tabs/tab_strip_model.h"
24 #include "chrome/common/chrome_switches.h"
25 #include "chrome/renderer/safe_browsing/features.h"
26 #include "chrome/renderer/safe_browsing/mock_feature_extractor_clock.h"
27 #include "chrome/renderer/safe_browsing/test_utils.h"
28 #include "chrome/test/base/in_process_browser_test.h"
29 #include "chrome/test/base/ui_test_utils.h"
30 #include "content/public/browser/interstitial_page.h"
31 #include "content/public/browser/web_contents.h"
32 #include "content/public/renderer/render_view.h"
33 #include "content/public/test/browser_test_utils.h"
34 #include "content/public/test/test_utils.h"
35 #include "net/dns/mock_host_resolver.h"
36 #include "net/test/embedded_test_server/embedded_test_server.h"
37 #include "net/test/embedded_test_server/http_request.h"
38 #include "net/test/embedded_test_server/http_response.h"
39 #include "testing/gmock/include/gmock/gmock.h"
40 #include "third_party/WebKit/public/platform/WebString.h"
41 #include "third_party/WebKit/public/web/WebFrame.h"
42 #include "third_party/WebKit/public/web/WebScriptSource.h"
43 #include "third_party/WebKit/public/web/WebView.h"
45 using ::testing::DoAll;
46 using ::testing::Invoke;
47 using ::testing::Return;
49 namespace {
51 // The first RenderFrame is routing ID 1, and the first RenderView is 2.
52 const int kRenderViewRoutingId = 2;
56 namespace safe_browsing {
58 class PhishingDOMFeatureExtractorTest : public InProcessBrowserTest {
59 public:
60 content::WebContents* GetWebContents() {
61 return browser()->tab_strip_model()->GetActiveWebContents();
64 // Helper for the SubframeRemoval test that posts a message to remove
65 // the iframe "frame1" from the document.
66 void ScheduleRemoveIframe() {
67 base::ThreadTaskRunnerHandle::Get()->PostTask(
68 FROM_HERE, base::Bind(&PhishingDOMFeatureExtractorTest::RemoveIframe,
69 weak_factory_.GetWeakPtr()));
72 protected:
73 PhishingDOMFeatureExtractorTest() : weak_factory_(this) {}
75 ~PhishingDOMFeatureExtractorTest() override {}
77 void SetUpCommandLine(base::CommandLine* command_line) override {
78 command_line->AppendSwitch(switches::kSingleProcess);
79 #if defined(OS_WIN)
80 // Don't want to try to create a GPU process.
81 command_line->AppendSwitch(switches::kDisableGpu);
82 #endif
85 void SetUpOnMainThread() override {
86 extractor_.reset(new PhishingDOMFeatureExtractor(
87 content::RenderView::FromRoutingID(kRenderViewRoutingId), &clock_));
89 ASSERT_TRUE(StartTestServer());
90 host_resolver()->AddRule("*", "127.0.0.1");
93 // Runs the DOMFeatureExtractor on the RenderView, waiting for the
94 // completion callback. Returns the success boolean from the callback.
95 bool ExtractFeatures(FeatureMap* features) {
96 success_ = false;
97 PostTaskToInProcessRendererAndWait(
98 base::Bind(&PhishingDOMFeatureExtractorTest::ExtractFeaturesInternal,
99 base::Unretained(this),
100 features));
101 return success_;
104 void ExtractFeaturesInternal(FeatureMap* features) {
105 scoped_refptr<content::MessageLoopRunner> message_loop =
106 new content::MessageLoopRunner;
107 extractor_->ExtractFeatures(
108 features,
109 base::Bind(&PhishingDOMFeatureExtractorTest::ExtractionDone,
110 base::Unretained(this),
111 message_loop->QuitClosure()));
112 message_loop->Run();
115 // Completion callback for feature extraction.
116 void ExtractionDone(const base::Closure& quit_closure,
117 bool success) {
118 success_ = success;
119 quit_closure.Run();
122 // Does the actual work of removing the iframe "frame1" from the document.
123 void RemoveIframe() {
124 content::RenderView* render_view =
125 content::RenderView::FromRoutingID(kRenderViewRoutingId);
126 blink::WebFrame* main_frame = render_view->GetWebView()->mainFrame();
127 ASSERT_TRUE(main_frame);
128 main_frame->executeScript(
129 blink::WebString(
130 "document.body.removeChild(document.getElementById('frame1'));"));
133 bool StartTestServer() {
134 CHECK(!embedded_test_server_);
135 embedded_test_server_.reset(new net::test_server::EmbeddedTestServer());
136 embedded_test_server_->RegisterRequestHandler(
137 base::Bind(&PhishingDOMFeatureExtractorTest::HandleRequest,
138 base::Unretained(this)));
139 return embedded_test_server_->InitializeAndWaitUntilReady();
142 scoped_ptr<net::test_server::HttpResponse> HandleRequest(
143 const net::test_server::HttpRequest& request) {
144 std::map<std::string, std::string>::const_iterator host_it =
145 request.headers.find("Host");
146 if (host_it == request.headers.end())
147 return scoped_ptr<net::test_server::HttpResponse>();
149 std::string url =
150 std::string("http://") + host_it->second + request.relative_url;
151 std::map<std::string, std::string>::const_iterator it =
152 responses_.find(url);
153 if (it == responses_.end())
154 return scoped_ptr<net::test_server::HttpResponse>();
156 scoped_ptr<net::test_server::BasicHttpResponse> http_response(
157 new net::test_server::BasicHttpResponse());
158 http_response->set_code(net::HTTP_OK);
159 http_response->set_content_type("text/html");
160 http_response->set_content(it->second);
161 return http_response.Pass();
164 GURL GetURL(const std::string& host, const std::string& path) {
165 GURL::Replacements replace;
166 replace.SetHostStr(host);
167 replace.SetPathStr(path);
168 return embedded_test_server_->base_url().ReplaceComponents(replace);
171 // Returns the URL that was loaded.
172 GURL LoadHtml(const std::string& host, const std::string& content) {
173 GURL url(GetURL(host, ""));
174 responses_[url.spec()] = content;
175 ui_test_utils::NavigateToURL(browser(), url);
176 return url;
179 // Map of url -> response body for network requests from the renderer.
180 // Any urls not in this map are served a 404 error.
181 std::map<std::string, std::string> responses_;
183 scoped_ptr<net::test_server::EmbeddedTestServer> embedded_test_server_;
184 MockFeatureExtractorClock clock_;
185 scoped_ptr<PhishingDOMFeatureExtractor> extractor_;
186 bool success_; // holds the success value from ExtractFeatures
187 base::WeakPtrFactory<PhishingDOMFeatureExtractorTest> weak_factory_;
190 IN_PROC_BROWSER_TEST_F(PhishingDOMFeatureExtractorTest, FormFeatures) {
191 // This test doesn't exercise the extraction timing.
192 EXPECT_CALL(clock_, Now()).WillRepeatedly(Return(base::TimeTicks::Now()));
194 FeatureMap expected_features;
195 expected_features.AddBooleanFeature(features::kPageHasForms);
196 expected_features.AddRealFeature(features::kPageActionOtherDomainFreq, 0.25);
197 expected_features.AddBooleanFeature(features::kPageHasTextInputs);
198 expected_features.AddBooleanFeature(features::kPageHasCheckInputs);
199 expected_features.AddBooleanFeature(features::kPageActionURL +
200 std::string("http://cgi.host.com/submit"));
201 expected_features.AddBooleanFeature(features::kPageActionURL +
202 std::string("http://other.com/"));
203 expected_features.AddBooleanFeature(features::kPageActionURL +
204 std::string("http://host.com:") +
205 base::IntToString(embedded_test_server_->port()) +
206 std::string("/query"));
208 FeatureMap features;
209 LoadHtml(
210 "host.com",
211 "<html><head><body>"
212 "<form action=\"query\"><input type=text><input type=checkbox></form>"
213 "<form action=\"http://cgi.host.com/submit\"></form>"
214 "<form action=\"http://other.com/\"></form>"
215 "<form action=\"query\"></form>"
216 "<form></form></body></html>");
217 ASSERT_TRUE(ExtractFeatures(&features));
218 ExpectFeatureMapsAreEqual(features, expected_features);
220 expected_features.Clear();
221 expected_features.AddBooleanFeature(features::kPageHasRadioInputs);
222 expected_features.AddBooleanFeature(features::kPageHasPswdInputs);
224 features.Clear();
225 LoadHtml(
226 "host.com",
227 "<html><head><body>"
228 "<input type=\"radio\"><input type=password></body></html>");
229 ASSERT_TRUE(ExtractFeatures(&features));
230 ExpectFeatureMapsAreEqual(features, expected_features);
232 expected_features.Clear();
233 expected_features.AddBooleanFeature(features::kPageHasTextInputs);
235 features.Clear();
236 LoadHtml(
237 "host.com",
238 "<html><head><body><input></body></html>");
239 ASSERT_TRUE(ExtractFeatures(&features));
240 ExpectFeatureMapsAreEqual(features, expected_features);
242 expected_features.Clear();
243 expected_features.AddBooleanFeature(features::kPageHasTextInputs);
245 features.Clear();
246 LoadHtml(
247 "host.com",
248 "<html><head><body><input type=\"invalid\"></body></html>");
249 ASSERT_TRUE(ExtractFeatures(&features));
250 ExpectFeatureMapsAreEqual(features, expected_features);
253 IN_PROC_BROWSER_TEST_F(PhishingDOMFeatureExtractorTest, LinkFeatures) {
254 // This test doesn't exercise the extraction timing.
255 EXPECT_CALL(clock_, Now()).WillRepeatedly(Return(base::TimeTicks::Now()));
257 FeatureMap expected_features;
258 expected_features.AddRealFeature(features::kPageExternalLinksFreq, 0.5);
259 expected_features.AddRealFeature(features::kPageSecureLinksFreq, 0.0);
260 expected_features.AddBooleanFeature(features::kPageLinkDomain +
261 std::string("chromium.org"));
263 FeatureMap features;
264 LoadHtml(
265 "www.host.com",
266 "<html><head><body>"
267 "<a href=\"http://www2.host.com/abc\">link</a>"
268 "<a name=page_anchor></a>"
269 "<a href=\"http://www.chromium.org/\">chromium</a>"
270 "</body></html");
271 ASSERT_TRUE(ExtractFeatures(&features));
272 ExpectFeatureMapsAreEqual(features, expected_features);
274 expected_features.Clear();
275 expected_features.AddRealFeature(features::kPageExternalLinksFreq, 0.25);
276 expected_features.AddRealFeature(features::kPageSecureLinksFreq, 0.5);
277 expected_features.AddBooleanFeature(features::kPageLinkDomain +
278 std::string("chromium.org"));
280 net::SpawnedTestServer https_server(
281 net::SpawnedTestServer::TYPE_HTTPS,
282 net::SpawnedTestServer::kLocalhost,
283 base::FilePath(FILE_PATH_LITERAL("chrome/test/data")));
284 ASSERT_TRUE(https_server.Start());
286 // The PhishingDOMFeatureExtractor depends on URLs being domains and not IPs,
287 // so use a domain.
288 std::string url_str = "https://host.com:";
289 url_str += base::IntToString(https_server.host_port_pair().port());
290 url_str += "/files/safe_browsing/secure_link_features.html";
291 ui_test_utils::NavigateToURL(browser(), GURL(url_str));
293 // Click through the certificate error interstitial.
294 content::InterstitialPage* interstitial_page =
295 GetWebContents()->GetInterstitialPage();
296 interstitial_page->Proceed();
297 content::WaitForLoadStop(GetWebContents());
299 features.Clear();
300 ASSERT_TRUE(ExtractFeatures(&features));
301 ExpectFeatureMapsAreEqual(features, expected_features);
304 // Flaky on Win/Linux. https://crbug.com/373155.
305 #if defined(OS_WIN) || defined(OS_LINUX)
306 #define MAYBE_ScriptAndImageFeatures DISABLED_ScriptAndImageFeatures
307 #else
308 #define MAYBE_ScriptAndImageFeatures ScriptAndImageFeatures
309 #endif
310 IN_PROC_BROWSER_TEST_F(PhishingDOMFeatureExtractorTest,
311 MAYBE_ScriptAndImageFeatures) {
312 // This test doesn't exercise the extraction timing.
313 EXPECT_CALL(clock_, Now()).WillRepeatedly(Return(base::TimeTicks::Now()));
315 FeatureMap expected_features;
316 expected_features.AddBooleanFeature(features::kPageNumScriptTagsGTOne);
318 FeatureMap features;
319 LoadHtml(
320 "host.com",
321 "<html><head><script></script><script></script></head></html>");
322 ASSERT_TRUE(ExtractFeatures(&features));
323 ExpectFeatureMapsAreEqual(features, expected_features);
325 expected_features.Clear();
326 expected_features.AddBooleanFeature(features::kPageNumScriptTagsGTOne);
327 expected_features.AddBooleanFeature(features::kPageNumScriptTagsGTSix);
328 expected_features.AddRealFeature(features::kPageImgOtherDomainFreq, 0.5);
330 features.Clear();
331 net::SpawnedTestServer https_server(
332 net::SpawnedTestServer::TYPE_HTTPS,
333 net::SpawnedTestServer::kLocalhost,
334 base::FilePath(FILE_PATH_LITERAL("chrome/test/data")));
335 ASSERT_TRUE(https_server.Start());
337 // The PhishingDOMFeatureExtractor depends on URLs being domains and not IPs,
338 // so use a domain.
339 std::string url_str = "https://host.com:";
340 url_str += base::IntToString(https_server.host_port_pair().port());
341 url_str += "/files/safe_browsing/secure_script_and_image.html";
342 ui_test_utils::NavigateToURL(browser(), GURL(url_str));
344 // Click through the certificate error interstitial.
345 content::InterstitialPage* interstitial_page =
346 GetWebContents()->GetInterstitialPage();
347 interstitial_page->Proceed();
348 content::WaitForLoadStop(GetWebContents());
350 ASSERT_TRUE(ExtractFeatures(&features));
351 ExpectFeatureMapsAreEqual(features, expected_features);
354 IN_PROC_BROWSER_TEST_F(PhishingDOMFeatureExtractorTest, SubFrames) {
355 // This test doesn't exercise the extraction timing.
356 EXPECT_CALL(clock_, Now()).WillRepeatedly(Return(base::TimeTicks::Now()));
358 // Test that features are aggregated across all frames.
360 std::string port = base::IntToString(embedded_test_server_->port());
361 responses_[GetURL("host2.com", "").spec()] =
362 "<html><head><script></script><body>"
363 "<form action=\"http://host4.com/\"><input type=checkbox></form>"
364 "<form action=\"http://host2.com/submit\"></form>"
365 "<a href=\"http://www.host2.com/home\">link</a>"
366 "<iframe src=\"nested.html\"></iframe>"
367 "<body></html>";
369 responses_[GetURL("host2.com", "nested.html").spec()] =
370 "<html><body><input type=password>"
371 "<a href=\"https://host4.com/\">link</a>"
372 "<a href=\"relative\">another</a>"
373 "</body></html>";
375 responses_[GetURL("host3.com", "").spec()] =
376 "<html><head><script></script><body>"
377 "<img src=\"http://host.com/123.png\">"
378 "</body></html>";
380 FeatureMap expected_features;
381 expected_features.AddBooleanFeature(features::kPageHasForms);
382 // Form action domains are compared to the URL of the document they're in,
383 // not the URL of the toplevel page. So http://host2.com/ has two form
384 // actions, one of which is external.
385 expected_features.AddRealFeature(features::kPageActionOtherDomainFreq, 0.5);
386 expected_features.AddBooleanFeature(features::kPageHasTextInputs);
387 expected_features.AddBooleanFeature(features::kPageHasPswdInputs);
388 expected_features.AddBooleanFeature(features::kPageHasCheckInputs);
389 expected_features.AddRealFeature(features::kPageExternalLinksFreq, 0.25);
390 expected_features.AddBooleanFeature(features::kPageLinkDomain +
391 std::string("host4.com"));
392 expected_features.AddRealFeature(features::kPageSecureLinksFreq, 0.25);
393 expected_features.AddBooleanFeature(features::kPageNumScriptTagsGTOne);
394 expected_features.AddRealFeature(features::kPageImgOtherDomainFreq, 1.0);
395 expected_features.AddBooleanFeature(features::kPageActionURL +
396 std::string("http://host2.com/submit"));
397 expected_features.AddBooleanFeature(features::kPageActionURL +
398 std::string("http://host4.com/"));
400 FeatureMap features;
401 std::string html(
402 "<html><body><input type=text><a href=\"info.html\">link</a>"
403 "<iframe src=\"http://host2.com:");
404 html += port;
405 html += std::string(
406 "/\"></iframe>"
407 "<iframe src=\"http://host3.com:");
408 html += port;
409 html += std::string("/\"></iframe></body></html>");
411 LoadHtml("host.com", html);
412 ASSERT_TRUE(ExtractFeatures(&features));
413 ExpectFeatureMapsAreEqual(features, expected_features);
416 // Test flakes with LSAN enabled. See http://crbug.com/373155.
417 #if defined(LEAK_SANITIZER)
418 #define MAYBE_Continuation DISABLED_Continuation
419 #else
420 #define MAYBE_Continuation Continuation
421 #endif
422 IN_PROC_BROWSER_TEST_F(PhishingDOMFeatureExtractorTest, MAYBE_Continuation) {
423 // For this test, we'll cause the feature extraction to run multiple
424 // iterations by incrementing the clock.
426 // This page has a total of 50 elements. For the external forms feature to
427 // be computed correctly, the extractor has to examine the whole document.
428 // Note: the empty HEAD is important -- WebKit will synthesize a HEAD if
429 // there isn't one present, which can be confusing for the element counts.
430 std::string response = "<html><head></head><body>"
431 "<form action=\"ondomain\"></form>";
432 for (int i = 0; i < 45; ++i) {
433 response.append("<p>");
435 response.append("<form action=\"http://host2.com/\"></form></body></html>");
437 // Advance the clock 6 ms every 10 elements processed, 10 ms between chunks.
438 // Note that this assumes kClockCheckGranularity = 10 and
439 // kMaxTimePerChunkMs = 10.
440 base::TimeTicks now = base::TimeTicks::Now();
441 EXPECT_CALL(clock_, Now())
442 // Time check at the start of extraction.
443 .WillOnce(Return(now))
444 // Time check at the start of the first chunk of work.
445 .WillOnce(Return(now))
446 // Time check after the first 10 elements.
447 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(6)))
448 // Time check after the next 10 elements. This is over the chunk
449 // time limit, so a continuation task will be posted.
450 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(12)))
451 // Time check at the start of the second chunk of work.
452 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(22)))
453 // Time check after resuming iteration for the second chunk.
454 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(24)))
455 // Time check after the next 10 elements.
456 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(30)))
457 // Time check after the next 10 elements. This will trigger another
458 // continuation task.
459 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(36)))
460 // Time check at the start of the third chunk of work.
461 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(46)))
462 // Time check after resuming iteration for the third chunk.
463 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(48)))
464 // Time check after the last 10 elements.
465 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(54)))
466 // A final time check for the histograms.
467 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(56)));
469 FeatureMap expected_features;
470 expected_features.AddBooleanFeature(features::kPageHasForms);
471 expected_features.AddRealFeature(features::kPageActionOtherDomainFreq, 0.5);
472 expected_features.AddBooleanFeature(features::kPageActionURL +
473 std::string("http://host.com:") +
474 base::IntToString(embedded_test_server_->port()) +
475 std::string("/ondomain"));
476 expected_features.AddBooleanFeature(features::kPageActionURL +
477 std::string("http://host2.com/"));
479 FeatureMap features;
480 LoadHtml("host.com", response);
481 ASSERT_TRUE(ExtractFeatures(&features));
482 ExpectFeatureMapsAreEqual(features, expected_features);
483 // Make sure none of the mock expectations carry over to the next test.
484 ::testing::Mock::VerifyAndClearExpectations(&clock_);
486 // Now repeat the test with the same page, but advance the clock faster so
487 // that the extraction time exceeds the maximum total time for the feature
488 // extractor. Extraction should fail. Note that this assumes
489 // kMaxTotalTimeMs = 500.
490 EXPECT_CALL(clock_, Now())
491 // Time check at the start of extraction.
492 .WillOnce(Return(now))
493 // Time check at the start of the first chunk of work.
494 .WillOnce(Return(now))
495 // Time check after the first 10 elements.
496 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(300)))
497 // Time check at the start of the second chunk of work.
498 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(350)))
499 // Time check after resuming iteration for the second chunk.
500 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(360)))
501 // Time check after the next 10 elements. This is over the limit.
502 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(600)))
503 // A final time check for the histograms.
504 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(620)));
506 features.Clear();
507 EXPECT_FALSE(ExtractFeatures(&features));
510 IN_PROC_BROWSER_TEST_F(PhishingDOMFeatureExtractorTest, SubframeRemoval) {
511 // In this test, we'll advance the feature extractor so that it is positioned
512 // inside an iframe, and have it pause due to exceeding the chunk time limit.
513 // Then, prior to continuation, the iframe is removed from the document.
514 // As currently implemented, this should finish extraction from the removed
515 // iframe document.
516 responses_[GetURL("host.com", "frame.html").spec()] =
517 "<html><body><p><p><p><input type=password></body></html>";
519 base::TimeTicks now = base::TimeTicks::Now();
520 EXPECT_CALL(clock_, Now())
521 // Time check at the start of extraction.
522 .WillOnce(Return(now))
523 // Time check at the start of the first chunk of work.
524 .WillOnce(Return(now))
525 // Time check after the first 10 elements. Enough time has passed
526 // to stop extraction. Schedule the iframe removal to happen as soon as
527 // the feature extractor returns control to the message loop.
528 .WillOnce(DoAll(
529 Invoke(this, &PhishingDOMFeatureExtractorTest::ScheduleRemoveIframe),
530 Return(now + base::TimeDelta::FromMilliseconds(21))))
531 // Time check at the start of the second chunk of work.
532 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(25)))
533 // Time check after resuming iteration for the second chunk.
534 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(27)))
535 // A final time check for the histograms.
536 .WillOnce(Return(now + base::TimeDelta::FromMilliseconds(33)));
538 FeatureMap expected_features;
539 expected_features.AddBooleanFeature(features::kPageHasForms);
540 expected_features.AddBooleanFeature(features::kPageHasPswdInputs);
542 FeatureMap features;
543 LoadHtml(
544 "host.com",
545 "<html><head></head><body>"
546 "<iframe src=\"frame.html\" id=\"frame1\"></iframe>"
547 "<form></form></body></html>");
548 ASSERT_TRUE(ExtractFeatures(&features));
549 ExpectFeatureMapsAreEqual(features, expected_features);
552 } // namespace safe_browsing