| // Copyright 2018 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 "components/autofill/core/browser/password_requirements_spec_fetcher_impl.h" |
| |
| #include "base/logging.h" |
| #include "base/test/bind_test_util.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/scoped_task_environment.h" |
| #include "components/autofill/core/browser/proto/password_requirements.pb.h" |
| #include "components/autofill/core/browser/proto/password_requirements_shard.pb.h" |
| #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" |
| #include "services/network/test/test_url_loader_factory.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace autofill { |
| |
| namespace { |
| |
| // URL prefix for spec requests. |
| #define SERVER_URL \ |
| "https://www.gstatic.com/chrome/autofill/password_generation_specs/" |
| |
| TEST(PasswordRequirementsSpecFetcherTest, FetchData) { |
| using ResultCode = PasswordRequirementsSpecFetcherImpl::ResultCode; |
| |
| // An empty spec is returned for all error cases (time outs, server responding |
| // with anything but HTTP_OK). |
| PasswordRequirementsSpec empty_spec; |
| |
| PasswordRequirementsSpec success_spec_for_example_com; |
| success_spec_for_example_com.set_min_length(17); |
| PasswordRequirementsSpec success_spec_for_m_example_com; |
| success_spec_for_m_example_com.set_min_length(18); |
| PasswordRequirementsSpec spec_for_ip; |
| success_spec_for_m_example_com.set_min_length(19); |
| PasswordRequirementsSpec success_spec_for_uber_example_com; |
| success_spec_for_uber_example_com.set_min_length(20); |
| |
| std::string serialized_shard; |
| PasswordRequirementsShard shard; |
| (*shard.mutable_specs())["example.com"] = success_spec_for_example_com; |
| (*shard.mutable_specs())["m.example.com"] = success_spec_for_m_example_com; |
| // This spec is stored in the buffer but is not expected to be processed. |
| // Only real hostnames are supposed to be parsed. |
| (*shard.mutable_specs())["192.168.1.1"] = spec_for_ip; |
| // Punycoded entry. |
| (*shard.mutable_specs())["xn--ber-example-shb.com"] = |
| success_spec_for_uber_example_com; |
| shard.SerializeToString(&serialized_shard); |
| |
| // If this magic timeout value is set, simulate a timeout. |
| const int kMagicTimeout = 10; |
| |
| struct { |
| // Name of the test for log output. |
| const char* test_name; |
| |
| // Origin for which the spec is fetched. |
| const char* origin; |
| |
| // Current configuration that would be set via Finch. |
| int generation = 1; |
| int prefix_length = 32; |
| int timeout = 1000; |
| |
| // Handler for the spec requests. |
| const char* requested_url; |
| std::string response_content; |
| net::HttpStatusCode response_status = net::HTTP_OK; |
| |
| // Expected spec. |
| PasswordRequirementsSpec* expected_spec; |
| ResultCode expected_result; |
| } tests[] = { |
| { |
| .test_name = "Business as usual", |
| .origin = "https://www.example.com", |
| // See echo -n example.com | md5sum | cut -b 1-4 |
| .requested_url = SERVER_URL "1/5aba", |
| .response_content = serialized_shard, |
| .expected_spec = &success_spec_for_example_com, |
| .expected_result = ResultCode::kFoundSpec, |
| }, |
| { |
| .test_name = "Parts before the eTLD+1 don't matter", |
| // m.example.com instead of www.example.com creates the same hash |
| // prefix. |
| .origin = "https://m.example.com", |
| .requested_url = SERVER_URL "1/5aba", |
| .response_content = serialized_shard, |
| // But shard contains a special entry for m.example.com, so verify |
| // that the more specific element is returned. |
| .expected_spec = &success_spec_for_m_example_com, |
| .expected_result = ResultCode::kFoundSpec, |
| }, |
| { |
| .test_name = "The generation is encoded in the url", |
| .origin = "https://www.example.com", |
| // Here the test differs from the default: |
| .generation = 2, |
| .requested_url = SERVER_URL "2/5aba", |
| .response_content = serialized_shard, |
| .expected_spec = &success_spec_for_example_com, |
| .expected_result = ResultCode::kFoundSpec, |
| }, |
| { |
| .test_name = "Shorter prefixes are reflected in the URL", |
| .origin = "https://m.example.com", |
| // The prefix "5abc" starts with 0b01011010. If the prefix is limited |
| // to the first 3 bits, b0100 = 0x4 remains and the rest is zeroed |
| // out. |
| .prefix_length = 3, |
| .requested_url = SERVER_URL "1/4000", |
| .response_content = serialized_shard, |
| .expected_spec = &success_spec_for_m_example_com, |
| .expected_result = ResultCode::kFoundSpec, |
| }, |
| { |
| .test_name = "Simulate a 404 response", |
| .origin = "https://www.example.com", |
| .requested_url = SERVER_URL "1/5aba", |
| .response_content = "Not found", |
| // If a file is not found on the server, the spec should be empty. |
| .response_status = net::HTTP_NOT_FOUND, |
| .expected_spec = &empty_spec, |
| .expected_result = ResultCode::kErrorFailedToFetch, |
| }, |
| { |
| .test_name = "Simulate a timeout", |
| .origin = "https://www.example.com", |
| // This simulates a timeout. |
| .timeout = kMagicTimeout, |
| // This makes sure that the server does not respond by itself as |
| // TestURLLoaderFactory reacts as if a response has been added to |
| // a URL. |
| .requested_url = SERVER_URL "dont_respond", |
| .response_content = serialized_shard, |
| .expected_spec = &empty_spec, |
| .expected_result = ResultCode::kErrorTimeout, |
| }, |
| { |
| .test_name = "Zero prefix", |
| .origin = "https://www.example.com", |
| // A zero prefix will be the hard-coded Finch default and should work. |
| .prefix_length = 0, |
| .requested_url = SERVER_URL "1/0000", |
| .response_content = serialized_shard, |
| .expected_spec = &success_spec_for_example_com, |
| .expected_result = ResultCode::kFoundSpec, |
| }, |
| { |
| .test_name = "IP addresses give the empty spec", |
| .origin = "http://192.168.1.1/", |
| // By setting the prefix to 0, the URL of the shard is predefined, |
| // but actually, not network request should be sent as password |
| // requirements are not supported for IP addresses. |
| .prefix_length = 0, |
| .requested_url = SERVER_URL "0/0000", |
| .response_content = serialized_shard, |
| .expected_spec = &empty_spec, |
| .expected_result = ResultCode::kErrorInvalidOrigin, |
| }, |
| { |
| .test_name = "IP addresses give the empty spec", |
| .origin = "chrome://settings", |
| // By setting the prefix to 0, the URL of the shard is predefined, |
| // but actually, not network request should be sent as password |
| // requirements are not supported the chrome:// scheme. |
| .prefix_length = 0, |
| .requested_url = SERVER_URL "0/0000", |
| .response_content = serialized_shard, |
| .expected_spec = &empty_spec, |
| .expected_result = ResultCode::kErrorInvalidOrigin, |
| }, |
| { |
| .test_name = "Trailing period is normalized", |
| // Despite the trailing '.', everything is like for example.com |
| .origin = "https://www.example.com.", |
| .requested_url = SERVER_URL "1/5aba", |
| .response_content = serialized_shard, |
| .expected_spec = &success_spec_for_example_com, |
| .expected_result = ResultCode::kFoundSpec, |
| }, |
| { |
| .test_name = "Test punycoding", |
| .origin = "https://www.über-example.com", |
| // See echo -n xn--ber-example-shb.com | md5sum | cut -b 1-4 |
| .requested_url = SERVER_URL "1/e5db", |
| .response_content = serialized_shard, |
| .expected_spec = &success_spec_for_uber_example_com, |
| .expected_result = ResultCode::kFoundSpec, |
| }, |
| { |
| .test_name = "Test no entry", |
| .origin = "https://www.no-entry.com", |
| // See echo -n no-entry.com | md5sum | cut -b 1-4 |
| .requested_url = SERVER_URL "1/7579", |
| .response_content = serialized_shard, |
| .expected_spec = &empty_spec, |
| .expected_result = ResultCode::kFoundNoSpec, |
| }, |
| }; |
| |
| for (const auto& test : tests) { |
| SCOPED_TRACE(test.test_name); |
| base::HistogramTester histogram_tester; |
| |
| base::test::ScopedTaskEnvironment environment( |
| base::test::ScopedTaskEnvironment::MainThreadType::MOCK_TIME); |
| network::TestURLLoaderFactory loader_factory; |
| loader_factory.AddResponse(test.requested_url, test.response_content, |
| test.response_status); |
| |
| // Data to be collected from the callback. |
| bool callback_called = false; |
| PasswordRequirementsSpec returned_spec; |
| |
| // Trigger the network request and record data of the callback. |
| PasswordRequirementsSpecFetcherImpl fetcher( |
| base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>( |
| &loader_factory), |
| test.generation, test.prefix_length, test.timeout); |
| auto callback = |
| base::BindLambdaForTesting([&](const PasswordRequirementsSpec& spec) { |
| callback_called = true; |
| returned_spec = spec; |
| }); |
| fetcher.Fetch(GURL(test.origin), callback); |
| |
| environment.RunUntilIdle(); |
| |
| if (test.timeout == kMagicTimeout) { |
| // Make sure that the request takes longer than the timeout and gets |
| // killed by the timer. |
| environment.FastForwardBy( |
| base::TimeDelta::FromMilliseconds(2 * kMagicTimeout)); |
| environment.RunUntilIdle(); |
| } |
| |
| ASSERT_TRUE(callback_called); |
| EXPECT_EQ(test.expected_spec->SerializeAsString(), |
| returned_spec.SerializeAsString()); |
| histogram_tester.ExpectUniqueSample( |
| "PasswordManager.RequirementsSpecFetcher.Result", test.expected_result, |
| 1u); |
| } |
| } |
| |
| // Test two requests to fetch the same shard before the network responded. |
| // In this case, only one network request should be sent. |
| TEST(PasswordRequirementsSpecFetcherTest, FetchDataInterleaved) { |
| for (bool simulate_timeout : {false, true}) { |
| SCOPED_TRACE(::testing::Message() |
| << "Simulate timeout? " << simulate_timeout); |
| |
| // Set up the data that will be served. |
| std::string serialized_shard; |
| PasswordRequirementsShard shard; |
| (*shard.mutable_specs())["a.com"].set_min_length(17); |
| (*shard.mutable_specs())["b.com"].set_min_length(18); |
| shard.SerializeToString(&serialized_shard); |
| |
| base::test::ScopedTaskEnvironment environment( |
| base::test::ScopedTaskEnvironment::MainThreadType::MOCK_TIME); |
| network::TestURLLoaderFactory loader_factory; |
| |
| // Target into which data will be written by the callback. |
| PasswordRequirementsSpec spec_for_a; |
| PasswordRequirementsSpec spec_for_b; |
| // Set some values to see whether they get overridden by the callback. |
| spec_for_a.set_min_length(1); |
| spec_for_b.set_min_length(1); |
| |
| const int kVersion = 1; |
| // With a prefix length of 0, we guarantee that only one shard exists |
| // and therefore that the requests go to the same endpoint and can be |
| // unified into one network request. |
| const size_t kPrefixLength = 0; |
| const int kTimeout = 1000; |
| PasswordRequirementsSpecFetcherImpl fetcher( |
| base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>( |
| &loader_factory), |
| kVersion, kPrefixLength, kTimeout); |
| |
| fetcher.Fetch( |
| GURL("http://a.com"), |
| base::BindLambdaForTesting( |
| [&](const PasswordRequirementsSpec& spec) { spec_for_a = spec; })); |
| fetcher.Fetch( |
| GURL("http://b.com"), |
| base::BindLambdaForTesting( |
| [&](const PasswordRequirementsSpec& spec) { spec_for_b = spec; })); |
| |
| EXPECT_EQ(1, loader_factory.NumPending()); |
| |
| if (simulate_timeout) { |
| environment.FastForwardBy( |
| base::TimeDelta::FromMilliseconds(2 * kTimeout)); |
| environment.RunUntilIdle(); |
| EXPECT_FALSE(spec_for_a.has_min_length()); |
| EXPECT_FALSE(spec_for_b.has_min_length()); |
| } else { |
| loader_factory.AddResponse(SERVER_URL "1/0000", serialized_shard, |
| net::HTTP_OK); |
| environment.RunUntilIdle(); |
| |
| EXPECT_EQ(17u, spec_for_a.min_length()); |
| EXPECT_EQ(18u, spec_for_b.min_length()); |
| } |
| } |
| } |
| |
| // In case of incognito mode, we won't have a URL loader factory. |
| // Test that an empty spec is returned by the spec fetcher in this case. |
| TEST(PasswordRequirementsSpecFetcherTest, FetchDataWithoutURLLoaderFactory) { |
| base::test::ScopedTaskEnvironment environment; |
| |
| // Target into which data will be written by the callback. |
| PasswordRequirementsSpec received_spec; |
| // Set some values to see whether they get overridden by the callback. |
| received_spec.set_min_length(1); |
| |
| const int kVersion = 1; |
| const size_t kPrefixLength = 0; |
| const int kTimeout = 1000; |
| PasswordRequirementsSpecFetcherImpl fetcher(nullptr, kVersion, kPrefixLength, |
| kTimeout); |
| |
| fetcher.Fetch( |
| GURL("http://a.com"), |
| base::BindLambdaForTesting( |
| [&](const PasswordRequirementsSpec& spec) { received_spec = spec; })); |
| environment.RunUntilIdle(); |
| EXPECT_FALSE(received_spec.has_min_length()); |
| } |
| |
| #undef SERVER_URL |
| |
| } // namespace |
| |
| } // namespace autofill |