blob: 8fbf0e222e9c804d020dae88de554191c7b921b4 [file] [log] [blame]
// 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/password_manager/core/browser/form_parsing/form_parser.h"
#include <stddef.h>
#include <algorithm>
#include <set>
#include <utility>
#include "base/optional.h"
#include "base/strings/string16.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "build/build_config.h"
#include "components/autofill/core/common/form_data.h"
#include "components/autofill/core/common/form_field_data.h"
#include "components/autofill/core/common/password_form.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using autofill::FieldPropertiesFlags;
using autofill::FormData;
using autofill::FormFieldData;
using autofill::PasswordForm;
using base::ASCIIToUTF16;
namespace password_manager {
namespace {
using UsernameDetectionMethod = FormDataParser::UsernameDetectionMethod;
// Use this value in FieldDataDescription.value to get an arbitrary unique value
// generated in GetFormDataAndExpectation().
constexpr char kNonimportantValue[] = "non-important unique";
// Use this in FieldDataDescription below to mark the expected username and
// password fields.
enum class ElementRole {
NONE,
USERNAME,
CURRENT_PASSWORD,
NEW_PASSWORD,
CONFIRMATION_PASSWORD
};
// Expected FormFieldData are constructed based on these descriptions.
struct FieldDataDescription {
// The |role*| fields state the expected role of the field. The
// |role_filling| speaks specifically about parsing in
// FormDataParser::Mode::kFilling only, the |role_saving| about
// FormDataParser::Mode::kSaving. If set, |role| overrides both of the
// others.
ElementRole role = ElementRole::NONE;
ElementRole role_filling = ElementRole::NONE;
ElementRole role_saving = ElementRole::NONE;
bool is_focusable = true;
bool is_enabled = true;
bool is_readonly = false;
autofill::FieldPropertiesMask properties_mask =
FieldPropertiesFlags::NO_FLAGS;
const char* autocomplete_attribute = nullptr;
const char* value = kNonimportantValue;
const char* name = kNonimportantValue;
const char* form_control_type = "text";
PasswordFieldPrediction prediction = {.type = autofill::MAX_VALID_FIELD_TYPE};
// If not -1, indicates on which rank among predicted usernames this should
// be. Unused ranks will be padded with unique IDs (not found in any fields).
int predicted_username = -1;
};
// Describes a test case for the parser.
struct FormParsingTestCase {
const char* description_for_logging;
std::vector<FieldDataDescription> fields;
// -1 just mean no checking.
int number_of_all_possible_passwords = -1;
int number_of_all_possible_usernames = -1;
// null means no checking
const autofill::ValueElementVector* all_possible_passwords = nullptr;
const autofill::ValueElementVector* all_possible_usernames = nullptr;
bool username_may_use_prefilled_placeholder = false;
base::Optional<FormDataParser::ReadonlyPasswordFields> readonly_status;
};
// Returns numbers which are distinct from each other within the scope of one
// test.
uint32_t GetUniqueId() {
static uint32_t counter = 10;
return counter++;
}
// Use to add a number suffix which is unique in the scope of the test.
base::string16 StampUniqueSuffix(const char* base_str) {
return ASCIIToUTF16(base_str) + ASCIIToUTF16("_") +
base::UintToString16(GetUniqueId());
}
// Describes which renderer IDs are expected for username/password fields
// identified in a PasswordForm.
struct ParseResultIds {
uint32_t username_id = FormFieldData::kNotSetFormControlRendererId;
uint32_t password_id = FormFieldData::kNotSetFormControlRendererId;
uint32_t new_password_id = FormFieldData::kNotSetFormControlRendererId;
uint32_t confirmation_password_id =
FormFieldData::kNotSetFormControlRendererId;
bool IsEmpty() const {
return username_id == FormFieldData::kNotSetFormControlRendererId &&
password_id == FormFieldData::kNotSetFormControlRendererId &&
new_password_id == FormFieldData::kNotSetFormControlRendererId &&
confirmation_password_id ==
FormFieldData::kNotSetFormControlRendererId;
}
};
// Updates |result| by putting |id| in the appropriate |result|'s field based
// on |role|.
void UpdateResultWithIdByRole(ParseResultIds* result,
uint32_t id,
ElementRole role) {
constexpr uint32_t kUnassigned = FormFieldData::kNotSetFormControlRendererId;
switch (role) {
case ElementRole::NONE:
// Nothing to update.
break;
case ElementRole::USERNAME:
DCHECK_EQ(kUnassigned, result->username_id);
result->username_id = id;
break;
case ElementRole::CURRENT_PASSWORD:
DCHECK_EQ(kUnassigned, result->password_id);
result->password_id = id;
break;
case ElementRole::NEW_PASSWORD:
DCHECK_EQ(kUnassigned, result->new_password_id);
result->new_password_id = id;
break;
case ElementRole::CONFIRMATION_PASSWORD:
DCHECK_EQ(kUnassigned, result->confirmation_password_id);
result->confirmation_password_id = id;
break;
}
}
// Creates a FormData to be fed to the parser. Includes FormFieldData as
// described in |fields_description|. Generates |fill_result| and |save_result|
// expectations about the result in FILLING and SAVING mode, respectively. Also
// fills |predictions| with the predictions contained in FieldDataDescriptions.
FormData GetFormDataAndExpectation(
const std::vector<FieldDataDescription>& fields_description,
FormPredictions* predictions,
ParseResultIds* fill_result,
ParseResultIds* save_result) {
FormData form_data;
form_data.action = GURL("http://example1.com");
form_data.origin = GURL("http://example2.com");
for (const FieldDataDescription& field_description : fields_description) {
FormFieldData field;
const uint32_t unique_id = GetUniqueId();
field.unique_renderer_id = unique_id;
field.id_attribute = StampUniqueSuffix("html_id");
if (field_description.name == kNonimportantValue) {
field.name = StampUniqueSuffix("html_name");
} else {
field.name = ASCIIToUTF16(field_description.name);
}
field.name_attribute = field.name;
field.form_control_type = field_description.form_control_type;
field.is_focusable = field_description.is_focusable;
field.is_enabled = field_description.is_enabled;
field.is_readonly = field_description.is_readonly;
field.properties_mask = field_description.properties_mask;
if (field_description.value == kNonimportantValue) {
field.value = StampUniqueSuffix("value");
} else {
field.value = ASCIIToUTF16(field_description.value);
}
if (field_description.autocomplete_attribute)
field.autocomplete_attribute = field_description.autocomplete_attribute;
form_data.fields.push_back(field);
if (field_description.role == ElementRole::NONE) {
UpdateResultWithIdByRole(fill_result, unique_id,
field_description.role_filling);
UpdateResultWithIdByRole(save_result, unique_id,
field_description.role_saving);
} else {
UpdateResultWithIdByRole(fill_result, unique_id, field_description.role);
UpdateResultWithIdByRole(save_result, unique_id, field_description.role);
}
if (field_description.prediction.type != autofill::MAX_VALID_FIELD_TYPE) {
(*predictions)[unique_id] = field_description.prediction;
}
if (field_description.predicted_username >= 0) {
size_t index = static_cast<size_t>(field_description.predicted_username);
if (form_data.username_predictions.size() <= index)
form_data.username_predictions.resize(index + 1);
form_data.username_predictions[index] = field.unique_renderer_id;
}
}
// Fill unused ranks in predictions with fresh IDs to check that those are
// correctly ignored. In real situation, this might correspond, e.g., to
// fields which were not fillable and hence dropped from the selection.
for (uint32_t& id : form_data.username_predictions) {
if (id == 0)
id = GetUniqueId();
}
return form_data;
}
// Check that |fields| has a field with unique renderer ID |renderer_id| which
// has the name |element_name| and value |*element_value|. If |renderer_id| is
// FormFieldData::kNotSetFormControlRendererId, then instead check that
// |element_name| and |*element_value| are empty. Set |element_kind| to identify
// the type of the field in logging: 'username', 'password', etc. The argument
// |element_value| can be null, in which case all checks involving it are
// skipped (useful for the confirmation password value, which is not represented
// in PasswordForm).
void CheckField(const std::vector<FormFieldData>& fields,
uint32_t renderer_id,
const base::string16& element_name,
const base::string16* element_value,
const char* element_kind) {
SCOPED_TRACE(testing::Message("Looking for element of kind ")
<< element_kind);
if (renderer_id == FormFieldData::kNotSetFormControlRendererId) {
EXPECT_EQ(base::string16(), element_name);
if (element_value)
EXPECT_EQ(base::string16(), *element_value);
return;
}
auto field_it = std::find_if(fields.begin(), fields.end(),
[renderer_id](const FormFieldData& field) {
return field.unique_renderer_id == renderer_id;
});
ASSERT_TRUE(field_it != fields.end())
<< "Could not find a field with renderer ID " << renderer_id;
// On iOS |unique_id| is used for identifying DOM elements, so the parser should
// return it. See crbug.com/896594
#if defined(OS_IOS)
EXPECT_EQ(element_name, field_it->unique_id);
#else
EXPECT_EQ(element_name, field_it->name);
#endif
if (element_value)
EXPECT_EQ(*element_value, field_it->value);
}
// Describes the |form_data| including field values and names. Use this in
// SCOPED_TRACE if other logging messages might refer to the form.
testing::Message DescribeFormData(const FormData& form_data) {
testing::Message result;
result << "Form contains " << form_data.fields.size() << " fields:\n";
for (const FormFieldData& field : form_data.fields) {
result << "type=" << field.form_control_type << ", name=" << field.name
<< ", value=" << field.value
<< ", unique id=" << field.unique_renderer_id << "\n";
}
return result;
}
// Check that the information distilled from |form_data| into |password_form| is
// matching |expectations|.
void CheckPasswordFormFields(const PasswordForm& password_form,
const FormData& form_data,
const ParseResultIds& expectations) {
SCOPED_TRACE(DescribeFormData(form_data));
CheckField(form_data.fields, expectations.username_id,
password_form.username_element, &password_form.username_value,
"username");
EXPECT_EQ(expectations.username_id,
password_form.username_element_renderer_id);
CheckField(form_data.fields, expectations.password_id,
password_form.password_element, &password_form.password_value,
"password");
EXPECT_EQ(expectations.password_id,
password_form.password_element_renderer_id);
CheckField(form_data.fields, expectations.new_password_id,
password_form.new_password_element,
&password_form.new_password_value, "new_password");
CheckField(form_data.fields, expectations.confirmation_password_id,
password_form.confirmation_password_element, nullptr,
"confirmation_password");
}
// Checks that in a vector of pairs of string16s, all the first parts of the
// pairs (which represent element values) are unique.
void CheckAllValuesUnique(const autofill::ValueElementVector& v) {
std::set<base::string16> all_values;
for (const auto pair : v) {
auto insertion = all_values.insert(pair.first);
EXPECT_TRUE(insertion.second) << pair.first << " is duplicated";
}
}
// Iterates over |test_cases|, creates a FormData for each, runs the parser and
// checks the results.
void CheckTestData(const std::vector<FormParsingTestCase>& test_cases) {
for (const FormParsingTestCase& test_case : test_cases) {
FormPredictions predictions;
ParseResultIds fill_result;
ParseResultIds save_result;
const FormData form_data = GetFormDataAndExpectation(
test_case.fields, &predictions, &fill_result, &save_result);
FormDataParser parser;
parser.set_predictions(std::move(predictions));
for (auto mode :
{FormDataParser::Mode::kFilling, FormDataParser::Mode::kSaving}) {
SCOPED_TRACE(
testing::Message("Test description: ")
<< test_case.description_for_logging << ", parsing mode = "
<< (mode == FormDataParser::Mode::kFilling ? "Filling" : "Saving"));
std::unique_ptr<PasswordForm> parsed_form = parser.Parse(form_data, mode);
const ParseResultIds& expected_ids =
mode == FormDataParser::Mode::kFilling ? fill_result : save_result;
if (expected_ids.IsEmpty()) {
EXPECT_FALSE(parsed_form) << "Expected no parsed results";
} else {
ASSERT_TRUE(parsed_form) << "Expected successful parsing";
EXPECT_EQ(PasswordForm::SCHEME_HTML, parsed_form->scheme);
EXPECT_FALSE(parsed_form->preferred);
EXPECT_FALSE(parsed_form->blacklisted_by_user);
EXPECT_EQ(PasswordForm::TYPE_MANUAL, parsed_form->type);
EXPECT_TRUE(parsed_form->has_renderer_ids);
EXPECT_EQ(test_case.username_may_use_prefilled_placeholder,
parsed_form->username_may_use_prefilled_placeholder);
CheckPasswordFormFields(*parsed_form, form_data, expected_ids);
CheckAllValuesUnique(parsed_form->all_possible_passwords);
CheckAllValuesUnique(parsed_form->other_possible_usernames);
if (test_case.number_of_all_possible_passwords >= 0) {
EXPECT_EQ(
static_cast<size_t>(test_case.number_of_all_possible_passwords),
parsed_form->all_possible_passwords.size());
}
if (test_case.all_possible_passwords) {
EXPECT_EQ(*test_case.all_possible_passwords,
parsed_form->all_possible_passwords);
}
if (test_case.number_of_all_possible_usernames >= 0) {
EXPECT_EQ(
static_cast<size_t>(test_case.number_of_all_possible_usernames),
parsed_form->other_possible_usernames.size());
}
if (test_case.all_possible_usernames) {
EXPECT_EQ(*test_case.all_possible_usernames,
parsed_form->other_possible_usernames);
}
}
if (test_case.readonly_status) {
EXPECT_EQ(*test_case.readonly_status, parser.readonly_status());
}
}
}
}
TEST(FormParserTest, NotPasswordForm) {
CheckTestData({
{
"No fields", {},
},
{
.description_for_logging = "No password fields",
.fields =
{
{.form_control_type = "text"}, {.form_control_type = "text"},
},
.number_of_all_possible_passwords = 0,
.number_of_all_possible_usernames = 0,
},
});
}
TEST(FormParserTest, SkipNotTextFields) {
CheckTestData({
{
"A 'select' between username and password fields",
{
{.role = ElementRole::USERNAME},
{.form_control_type = "select"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
.number_of_all_possible_passwords = 1,
.number_of_all_possible_usernames = 1,
},
});
}
TEST(FormParserTest, OnlyPasswordFields) {
CheckTestData({
{
.description_for_logging = "1 password field",
.fields =
{
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
.number_of_all_possible_passwords = 1,
.number_of_all_possible_usernames = 0,
},
{
"2 password fields, new and confirmation password",
{
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.value = "pw"},
{.role = ElementRole::CONFIRMATION_PASSWORD,
.form_control_type = "password",
.value = "pw"},
},
},
{
"2 password fields, current and new password",
{
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.value = "pw1"},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.value = "pw2"},
},
},
{
"3 password fields, current, new, confirm password",
{
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.value = "pw1"},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.value = "pw2"},
{.role = ElementRole::CONFIRMATION_PASSWORD,
.form_control_type = "password",
.value = "pw2"},
},
},
{
.description_for_logging = "3 password fields with different values",
.fields =
{
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.value = "pw1"},
{.form_control_type = "password", .value = "pw2"},
{.form_control_type = "password", .value = "pw3"},
},
.number_of_all_possible_passwords = 3,
},
{
"4 password fields, only the first 3 are considered",
{
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.value = "pw1"},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.value = "pw2"},
{.role = ElementRole::CONFIRMATION_PASSWORD,
.form_control_type = "password",
.value = "pw2"},
{.form_control_type = "password", .value = "pw3"},
},
},
{
"4 password fields, 4th same value as 3rd and 2nd, only the first 3 "
"are considered",
{
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.value = "pw1"},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.value = "pw2"},
{.role = ElementRole::CONFIRMATION_PASSWORD,
.form_control_type = "password",
.value = "pw2"},
{.form_control_type = "password", .value = "pw2"},
},
},
{
"4 password fields, all same value",
{
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.value = "pw"},
{.form_control_type = "password", .value = "pw"},
{.form_control_type = "password", .value = "pw"},
{.form_control_type = "password", .value = "pw"},
},
},
});
}
TEST(FormParserTest, TestFocusability) {
CheckTestData({
{
"non-focusable fields are considered when there are no focusable "
"fields",
{
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.is_focusable = false},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.is_focusable = false},
},
},
{
"non-focusable should be skipped when there are focusable fields",
{
{.form_control_type = "password", .is_focusable = false},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.is_focusable = true},
},
},
{
"non-focusable text fields before password",
{
{.form_control_type = "text", .is_focusable = false},
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.is_focusable = false},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.is_focusable = true},
},
.number_of_all_possible_usernames = 2,
},
{
"focusable and non-focusable text fields before password",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.is_focusable = true},
{.form_control_type = "text", .is_focusable = false},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.is_focusable = true},
},
},
{
.description_for_logging = "many passwords, some of them focusable",
.fields =
{
{.form_control_type = "password", .is_focusable = false},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.is_focusable = true},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.is_focusable = true,
.value = "pw"},
{.form_control_type = "password", .is_focusable = false},
{.form_control_type = "password", .is_focusable = false},
{.form_control_type = "password", .is_focusable = false},
{.form_control_type = "password", .is_focusable = false},
{.role = ElementRole::CONFIRMATION_PASSWORD,
.form_control_type = "password",
.is_focusable = true,
.value = "pw"},
{.form_control_type = "password", .is_focusable = false},
{.form_control_type = "password", .is_focusable = false},
},
// 9 distinct values in 10 password fields:
.number_of_all_possible_passwords = 9,
},
});
}
TEST(FormParserTest, TextAndPasswordFields) {
CheckTestData({
{
"Simple empty sign-in form",
// Forms with empty fields cannot be saved, so the parsing result for
// saving is empty.
{
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text",
.value = ""},
{.role_filling = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.value = ""},
},
// all_possible_* only count fields with non-empty values.
.number_of_all_possible_passwords = 0,
.number_of_all_possible_usernames = 0,
},
{
.description_for_logging = "Simple sign-in form with filled data",
.fields =
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
.number_of_all_possible_passwords = 1,
},
{
"Empty sign-in form with an extra text field",
{
{.form_control_type = "text", .value = ""},
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text",
.value = ""},
{.role_filling = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.value = ""},
},
},
{
"Non-empty sign-in form with an extra text field",
{
{.role_saving = ElementRole::USERNAME,
.form_control_type = "text"},
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text",
.value = ""},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
{
"Empty sign-in form with an extra invisible text field",
{
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text",
.value = ""},
{.form_control_type = "text", .is_focusable = false, .value = ""},
{.role_filling = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.value = ""},
},
},
{
"Non-empty sign-in form with an extra invisible text field",
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.form_control_type = "text", .is_focusable = false},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
{
"Simple empty sign-in form with empty username",
// Filled forms with a username field which is left empty are
// suspicious. The parser will just omit the username altogether.
{
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text",
.value = ""},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
{
"Simple empty sign-in form with empty password",
// Empty password, nothing to save.
{
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text"},
{.role_filling = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.value = ""},
},
},
});
}
TEST(FormParserTest, TestAutocomplete) {
CheckTestData({
{
.description_for_logging =
"All possible password autocomplete attributes and some fields "
"without autocomplete",
.fields =
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.autocomplete_attribute = "username"},
{.form_control_type = "text"},
{.form_control_type = "password"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "current-password"},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "new-password",
.value = "np"},
{.form_control_type = "password"},
{.role = ElementRole::CONFIRMATION_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "new-password",
.value = "np"},
},
// 4 distinct password values in 5 password fields
.number_of_all_possible_passwords = 4,
},
{
.description_for_logging =
"Non-password autocomplete attributes are skipped",
.fields =
{
{.form_control_type = "text",
.autocomplete_attribute = "email"},
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.value = "pw"},
{.role = ElementRole::CONFIRMATION_PASSWORD,
.form_control_type = "password",
.value = "pw"},
// NB: 'password' is not a valid autocomplete type hint.
{.form_control_type = "password",
.autocomplete_attribute = "password"},
},
.number_of_all_possible_passwords = 3,
.number_of_all_possible_usernames = 2,
},
{
"Basic heuristics kick in if autocomplete analysis fails",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.autocomplete_attribute = "email"},
// NB: 'password' is not a valid autocomplete type hint.
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "password"},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password"},
},
},
{
"Partial autocomplete analysis fails if no passwords are found",
// The attribute 'username' is ignored, because there was no password
// marked up.
{
{.form_control_type = "text",
.autocomplete_attribute = "username"},
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
{
"Multiple username autocomplete attributes, fallback to base "
"heuristics",
{
{.form_control_type = "text",
.autocomplete_attribute = "username"},
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.autocomplete_attribute = "username"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "current-password"},
},
},
{
"Parsing complex autocomplete attributes",
{
// Valid information about form sections, in addition to the
// username hint.
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.autocomplete_attribute = "section-test billing username"},
{.form_control_type = "text"},
// Invalid composition, but the parser is simplistic and just
// grabs the last token.
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "new-password current-password"},
{.form_control_type = "password"},
},
},
{
"Ignored autocomplete attributes",
{
// 'off' is ignored.
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.autocomplete_attribute = "off"},
// Invalid composition, the parser ignores all but the last token.
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "new-password abc"},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password"},
},
},
{
"Swapped username/password autocomplete attributes",
// Swap means ignoring autocomplete analysis and falling back to basic
// heuristics.
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.autocomplete_attribute = "current-password"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "username"},
},
},
{
"Autocomplete mark-up overrides visibility",
{
{.role = ElementRole::USERNAME,
.is_focusable = false,
.form_control_type = "text",
.autocomplete_attribute = "username"},
{.is_focusable = true, .form_control_type = "text"},
{.is_focusable = true, .form_control_type = "password"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.is_focusable = false,
.autocomplete_attribute = "current-password"},
},
},
});
}
TEST(FormParserTest, DisabledFields) {
CheckTestData({
{
.description_for_logging = "The disabled attribute is ignored",
.fields =
{
{.is_enabled = true, .form_control_type = "text"},
{.role = ElementRole::USERNAME,
.is_enabled = false,
.form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.is_enabled = false},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.is_enabled = true},
},
.number_of_all_possible_passwords = 2,
},
});
}
TEST(FormParserTest, SkippingFieldsWithCreditCardFields) {
CheckTestData({
{
"Simple form, all fields are credit-card-related",
{
{.form_control_type = "text",
.autocomplete_attribute = "cc-name"},
{.form_control_type = "password",
.autocomplete_attribute = "cc-any-string"},
},
},
{
.description_for_logging = "Non-CC fields are considered",
.fields =
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.form_control_type = "text",
.autocomplete_attribute = "cc-name"},
{.form_control_type = "password",
.autocomplete_attribute = "cc-any-string"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
.number_of_all_possible_passwords = 2,
},
});
}
TEST(FormParserTest, ReadonlyFields) {
CheckTestData({
{
"For usernames, readonly does not matter",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.is_readonly = true},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
{
"For passwords, readonly means: 'give up', perhaps there is a "
"virtual keyboard, filling might be ignored",
{
{.form_control_type = "text"},
{.form_control_type = "password", .is_readonly = true},
},
},
{
"But correctly marked passwords are accepted even if readonly",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.autocomplete_attribute = "username"},
{.role = ElementRole::NEW_PASSWORD,
.autocomplete_attribute = "new-password",
.form_control_type = "password",
.is_readonly = true},
{.role = ElementRole::CONFIRMATION_PASSWORD,
.autocomplete_attribute = "new-password",
.form_control_type = "password",
.is_readonly = true},
{.role = ElementRole::CURRENT_PASSWORD,
.autocomplete_attribute = "current-password",
.form_control_type = "password",
.is_readonly = true},
},
},
{
.description_for_logging = "And passwords already filled by user or "
"Chrome on pageload are accepted even if "
"readonly",
.fields =
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.properties_mask =
FieldPropertiesFlags::AUTOFILLED_ON_PAGELOAD,
.form_control_type = "password",
.is_readonly = true},
{.role = ElementRole::NEW_PASSWORD,
.properties_mask = FieldPropertiesFlags::USER_TYPED,
.form_control_type = "password",
.is_readonly = true},
{.form_control_type = "password", .is_readonly = true},
},
.number_of_all_possible_passwords = 3,
},
{
.description_for_logging = "And passwords already filled by user or "
"Chrome with FOAS are accepted even if "
"readonly",
.fields =
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.properties_mask =
FieldPropertiesFlags::AUTOFILLED_ON_USER_TRIGGER,
.form_control_type = "password",
.is_readonly = true},
{.role = ElementRole::NEW_PASSWORD,
.properties_mask = FieldPropertiesFlags::USER_TYPED,
.form_control_type = "password",
.is_readonly = true},
{.form_control_type = "password", .is_readonly = true},
},
.number_of_all_possible_passwords = 3,
},
});
}
TEST(FormParserTest, ServerHints) {
CheckTestData({
{
"Empty predictions don't cause panic",
{
{.form_control_type = "text"},
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
{
"Username-only predictions are ignored",
{
{.form_control_type = "text",
.prediction = {.type = autofill::USERNAME,
.may_use_prefilled_placeholder = true}},
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
{
"Simple predictions work",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME_AND_EMAIL_ADDRESS,
.may_use_prefilled_placeholder = true}},
{.form_control_type = "text"},
{.form_control_type = "password"},
{.role = ElementRole::CURRENT_PASSWORD,
.prediction = {.type = autofill::PASSWORD,
.may_use_prefilled_placeholder = true},
.form_control_type = "password"},
},
.username_may_use_prefilled_placeholder = true,
},
{
.description_for_logging = "Longer predictions work",
.fields =
{
{.role = ElementRole::USERNAME,
.prediction = {.type = autofill::USERNAME},
.form_control_type = "text"},
{.form_control_type = "text"},
{.form_control_type = "password"},
{.role = ElementRole::NEW_PASSWORD,
.prediction = {.type = autofill::ACCOUNT_CREATION_PASSWORD},
.form_control_type = "password"},
{.role = ElementRole::CONFIRMATION_PASSWORD,
.prediction = {.type = autofill::CONFIRMATION_PASSWORD},
.form_control_type = "password"},
{.role = ElementRole::CURRENT_PASSWORD,
.prediction = {.type = autofill::PASSWORD},
.form_control_type = "password"},
},
.number_of_all_possible_passwords = 4,
},
});
}
TEST(FormParserTest, Interactability) {
CheckTestData({
{
"If all fields are hidden, all are considered",
{
{.form_control_type = "text", .is_focusable = false},
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.is_focusable = false},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.is_focusable = false},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.is_focusable = false},
},
},
{
.description_for_logging =
"If some fields are hidden, only visible are considered",
.fields =
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.is_focusable = true},
{.form_control_type = "text", .is_focusable = false},
{.form_control_type = "password", .is_focusable = false},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.is_focusable = true},
},
.number_of_all_possible_passwords = 2,
},
{
.description_for_logging =
"If user typed somewhere, only typed-into fields are considered, "
"even if not currently visible",
.fields =
{
{.role = ElementRole::USERNAME,
.properties_mask = FieldPropertiesFlags::USER_TYPED,
.form_control_type = "text",
.is_focusable = false},
{.form_control_type = "text", .is_focusable = true},
{.form_control_type = "password", .is_focusable = false},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.properties_mask = FieldPropertiesFlags::AUTOFILLED,
.is_focusable = true},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.properties_mask = FieldPropertiesFlags::USER_TYPED,
.is_focusable = true},
},
.number_of_all_possible_passwords = 3,
},
{
"Interactability for usernames is only considered before the first "
"relevant password. That way, if, e.g., the username gets filled and "
"hidden (to let the user enter password), and there is another text "
"field visible below, the maximum Interactability won't end up being "
"kPossible, which would exclude the hidden username.",
{
{.role = ElementRole::USERNAME,
.properties_mask = FieldPropertiesFlags::AUTOFILLED,
.form_control_type = "text",
.is_focusable = false},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.properties_mask = FieldPropertiesFlags::AUTOFILLED,
.is_focusable = true},
{.form_control_type = "text", .is_focusable = true, .value = ""},
},
},
{
"Interactability also matters for HTML classifier.",
{
{.form_control_type = "text",
.is_focusable = false,
.predicted_username = 0},
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.is_focusable = true},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.is_focusable = true},
},
},
});
}
TEST(FormParserTest, AllPossiblePasswords) {
const autofill::ValueElementVector kPasswords = {
{ASCIIToUTF16("a"), ASCIIToUTF16("p1")},
{ASCIIToUTF16("b"), ASCIIToUTF16("p3")},
};
const autofill::ValueElementVector kUsernames = {
{ASCIIToUTF16("b"), ASCIIToUTF16("chosen")},
{ASCIIToUTF16("a"), ASCIIToUTF16("first")},
};
CheckTestData({
{
.description_for_logging = "It is always the first field name which "
"is associated with a duplicated password "
"value",
.fields =
{
{.form_control_type = "password", .name = "p1", .value = "a"},
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.name = "chosen",
.value = "b",
.autocomplete_attribute = "username"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "current-password",
.value = "a"},
{.form_control_type = "text", .name = "first", .value = "a"},
{.form_control_type = "text", .value = "a"},
{.form_control_type = "password", .name = "p3", .value = "b"},
{.form_control_type = "password", .value = "b"},
},
.number_of_all_possible_passwords = 2,
.all_possible_passwords = &kPasswords,
.number_of_all_possible_usernames = 2,
.all_possible_usernames = &kUsernames,
},
{
.description_for_logging =
"Empty values don't get added to all_possible_passwords",
.fields =
{
{.form_control_type = "password", .value = ""},
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text",
.autocomplete_attribute = "username"},
{.role_filling = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "current-password",
.value = ""},
{.form_control_type = "text"},
{.form_control_type = "text"},
{.form_control_type = "password", .value = ""},
{.form_control_type = "password", .value = ""},
},
.number_of_all_possible_passwords = 0,
},
{
.description_for_logging = "Empty values don't get added to "
"all_possible_passwords even if form gets "
"parsed",
.fields =
{
{.form_control_type = "password", .value = ""},
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.autocomplete_attribute = "username"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "current-password"},
{.form_control_type = "text"},
{.form_control_type = "text"},
{.form_control_type = "password", .value = ""},
{.form_control_type = "password", .value = ""},
},
.number_of_all_possible_passwords = 1,
},
{
.description_for_logging =
"A particular type of a squashed form (sign-in + sign-up)",
.fields =
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.autocomplete_attribute = "username"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "current-password"},
{.form_control_type = "text"},
{.form_control_type = "text"},
{.form_control_type = "password"},
{.form_control_type = "password"},
},
.number_of_all_possible_passwords = 3,
},
{
.description_for_logging = "A strange but not squashed form",
.fields =
{
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
{.form_control_type = "text"},
{.form_control_type = "text"},
{.form_control_type = "password"},
{.form_control_type = "password"},
{.form_control_type = "password"},
},
.number_of_all_possible_passwords = 4,
},
});
}
TEST(FormParserTest, UsernamePredictions) {
CheckTestData({
{
"Username prediction overrides structure",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.predicted_username = 0},
{.form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
{
"Username prediction does not override structure if empty and mode "
"is SAVING",
{
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text",
.predicted_username = 2,
.value = ""},
{.role_saving = ElementRole::USERNAME,
.form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
{
"Username prediction does not override autocomplete analysis",
{
{.form_control_type = "text", .predicted_username = 0},
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.autocomplete_attribute = "username"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "current-password"},
},
},
{
"Username prediction does not override server hints",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME_AND_EMAIL_ADDRESS}},
{.form_control_type = "text", .predicted_username = 0},
{.role = ElementRole::CURRENT_PASSWORD,
.prediction = {.type = autofill::PASSWORD},
.form_control_type = "password"},
},
},
{
"Username prediction order matters",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.predicted_username = 1},
{.form_control_type = "text", .predicted_username = 4},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
});
}
// In some situations, server hints or autocomplete mark-up do not provide the
// username might be omitted. Sometimes this is a truthful signal (there might
// be no username despite the presence of plain text fields), but often this is
// just incomplete data. In the long term, the server hints should be complete
// and also cover cases when the autocomplete mark-up is lacking; at that point,
// the parser should just trust that the signal is truthful. Until then,
// however, the parser is trying to complement the signal with its structural
// heuristics.
TEST(FormParserTest, ComplementingResults) {
CheckTestData({
{
"Current password from autocomplete analysis, username from basic "
"heuristics",
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "current-password"},
},
},
{
"New and confirmation passwords from server, username from basic "
"heuristics",
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CONFIRMATION_PASSWORD,
.prediction = {.type = autofill::CONFIRMATION_PASSWORD},
.form_control_type = "password"},
{.form_control_type = "text"},
{.role = ElementRole::NEW_PASSWORD,
.prediction = {.type = autofill::NEW_PASSWORD},
.form_control_type = "password"},
},
},
{
"No password from server still means that serve hints are ignored.",
{
{.prediction = {.type = autofill::USERNAME_AND_EMAIL_ADDRESS},
.form_control_type = "text"},
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
});
}
// Until autofill server learns to provide CVC-related hints, the parser should
// try to get the hint from the field names.
TEST(FormParserTest, CVC) {
CheckTestData({
{
"Name of 'verification_type' matches the CVC pattern.",
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.form_control_type = "text", .name = "verification_type"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
});
}
// Check that "readonly status" is reported accordingly.
TEST(FormParserTest, ReadonlyStatus) {
CheckTestData({
{
"Server hints prevent heuristics from using readonly.",
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.prediction = {.type = autofill::PASSWORD},
.is_readonly = true,
.form_control_type = "password"},
},
.readonly_status =
FormDataParser::ReadonlyPasswordFields::kNoHeuristics,
},
{
"Autocomplete attributes prevent heuristics from using readonly.",
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.autocomplete_attribute = "current-password",
.is_readonly = true,
.form_control_type = "password"},
},
.readonly_status =
FormDataParser::ReadonlyPasswordFields::kNoHeuristics,
},
{
"No password fields are a special case of not going through local "
"heuristics.",
{
{.form_control_type = "text"},
},
.readonly_status =
FormDataParser::ReadonlyPasswordFields::kNoHeuristics,
},
{
"No readonly passwords ignored.",
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
// While readonly, this field is not ignored because it was
// autofilled before.
.is_readonly = true,
.properties_mask = FieldPropertiesFlags::AUTOFILLED_ON_PAGELOAD,
.form_control_type = "password"},
{.role = ElementRole::NEW_PASSWORD,
.is_readonly = false,
.form_control_type = "password"},
},
.readonly_status =
FormDataParser::ReadonlyPasswordFields::kNoneIgnored,
},
{
"Some readonly passwords ignored.",
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.is_readonly = true, .form_control_type = "password"},
{.role = ElementRole::CURRENT_PASSWORD,
.is_readonly = false,
.form_control_type = "password"},
},
.readonly_status =
FormDataParser::ReadonlyPasswordFields::kSomeIgnored,
},
{
"All readonly passwords ignored.",
{
{.form_control_type = "text"},
{.is_readonly = true, .form_control_type = "password"},
},
.readonly_status =
FormDataParser::ReadonlyPasswordFields::kAllIgnored,
},
});
}
// Check that empty values are ignored when parsing for saving.
TEST(FormParserTest, NoEmptyValues) {
CheckTestData({
{
"Server hints overridden for non-empty values.",
{
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME},
.value = ""},
{.role_saving = ElementRole::USERNAME,
.form_control_type = "text"},
{.role_saving = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
{.role_filling = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::ACCOUNT_CREATION_PASSWORD},
.value = ""},
},
},
{
"Autocomplete attributes overridden for non-empty values.",
{
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text",
.autocomplete_attribute = "username",
.value = ""},
{.role_saving = ElementRole::USERNAME,
.form_control_type = "text"},
{.role_filling = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "current-password",
.value = ""},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "new-password"},
},
},
{
"Structure heuristics overridden for non-empty values.",
{
{.role_saving = ElementRole::USERNAME,
.form_control_type = "text"},
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text",
.value = ""},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
{.role_filling = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.value = ""},
},
},
});
}
// Check that multiple usernames in server hints are handled properly.
TEST(FormParserTest, MultipleUsernames) {
CheckTestData({
{
"More than two usernames are ignored.",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::PASSWORD}},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::ACCOUNT_CREATION_PASSWORD}},
},
},
{
"No current passwod -> ignore additional usernames.",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::ACCOUNT_CREATION_PASSWORD}},
},
},
{
"2 current passwods -> ignore additional usernames.",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::PASSWORD}},
{.form_control_type = "password",
.prediction = {.type = autofill::PASSWORD}},
},
},
{
"No new passwod -> ignore additional usernames.",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::PASSWORD}},
},
},
{
"Two usernames in sign-in, sign-up order.",
{
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.role_saving = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::PASSWORD}},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::ACCOUNT_CREATION_PASSWORD}},
},
},
{
"Two usernames in sign-up, sign-in order.",
{
{.role_saving = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::ACCOUNT_CREATION_PASSWORD}},
{.form_control_type = "password",
.prediction = {.type = autofill::ACCOUNT_CREATION_PASSWORD}},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::PASSWORD}},
},
},
{
"Two usernames in sign-in, sign-up order; sign-in is pre-filled.",
{
{.role_filling = ElementRole::USERNAME,
.form_control_type = "text",
.properties_mask = FieldPropertiesFlags::AUTOFILLED_ON_PAGELOAD,
.prediction = {.type = autofill::USERNAME}},
{.role_saving = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::PASSWORD}},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::ACCOUNT_CREATION_PASSWORD}},
},
},
});
}
// If multiple hints for new-password fields are given (e.g., because of more
// fields having the same signature), the first one should be marked as
// new-password. That way the generation can be offered before the user has
// thought of and typed their new password elsewhere. See
// https://crbug.com/902700 for more details.
TEST(FormParserTest, NewPasswordFirst) {
CheckTestData({
{
"More than two usernames are ignored.",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.role = ElementRole::NEW_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::ACCOUNT_CREATION_PASSWORD}},
{.form_control_type = "password",
.prediction = {.type = autofill::ACCOUNT_CREATION_PASSWORD}},
},
},
});
}
TEST(FormParserTest, HistogramsForUsernameDetectionMethod) {
struct HistogramTestCase {
FormParsingTestCase parsing_data;
UsernameDetectionMethod expected_method;
} kHistogramTestCases[] = {
{
{
"No username",
{
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::PASSWORD}},
},
},
UsernameDetectionMethod::kNoUsernameDetected,
},
{
{
"Reporting server analysis",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.prediction = {.type = autofill::USERNAME}},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::PASSWORD}},
},
},
UsernameDetectionMethod::kServerSidePrediction,
},
{
{
"Reporting autocomplete analysis",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.autocomplete_attribute = "username"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "current-password"},
},
},
UsernameDetectionMethod::kAutocompleteAttribute,
},
{
{
"Reporting HTML classifier",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.predicted_username = 0},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
UsernameDetectionMethod::kHtmlBasedClassifier,
},
{
{
"Reporting basic heuristics",
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password"},
},
},
UsernameDetectionMethod::kBaseHeuristic,
},
{
{
"Mixing server analysis on password and HTML classifier on "
"username is reported as HTML classifier",
{
{.role = ElementRole::USERNAME,
.form_control_type = "text",
.predicted_username = 0},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.prediction = {.type = autofill::PASSWORD}},
},
},
UsernameDetectionMethod::kHtmlBasedClassifier,
},
{
{
"Mixing autocomplete analysis on password and basic heuristics "
"on username is reported as basic heuristics",
{
{.role = ElementRole::USERNAME, .form_control_type = "text"},
{.role = ElementRole::CURRENT_PASSWORD,
.form_control_type = "password",
.autocomplete_attribute = "current-password"},
},
},
UsernameDetectionMethod::kBaseHeuristic,
},
};
for (const HistogramTestCase& histogram_test_case : kHistogramTestCases) {
base::HistogramTester tester;
CheckTestData({histogram_test_case.parsing_data});
// Expect two samples, because parsing is done once for filling and once for
// saving mode.
SCOPED_TRACE(histogram_test_case.parsing_data.description_for_logging);
tester.ExpectUniqueSample("PasswordManager.UsernameDetectionMethod",
histogram_test_case.expected_method,
2);
}
}
TEST(FormParserTest, GetSignonRealm) {
struct TestCase {
const char* input;
const char* expected_output;
} test_cases[]{
{"http://example.com/", "http://example.com/"},
{"http://example.com/signup", "http://example.com/"},
{"https://google.com/auth?a=1#b", "https://google.com/"},
};
for (const TestCase& test_case : test_cases) {
SCOPED_TRACE(testing::Message("Input: ")
<< test_case.input << " "
<< "Expected output: " << test_case.expected_output);
GURL input(test_case.input);
EXPECT_EQ(test_case.expected_output, GetSignonRealm(input));
}
}
} // namespace
} // namespace password_manager