diff options
author | stuartmorgan@google.com <stuartmorgan@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-16 17:26:11 +0000 |
---|---|---|
committer | stuartmorgan@google.com <stuartmorgan@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-07-16 17:26:11 +0000 |
commit | 4cbf4e5faca5c6d01906d0bbf1092c52f5435d4e (patch) | |
tree | 7205364030623ab74137816f3177403c85db0a84 /chrome/browser/password_manager | |
parent | aa82249f5670f88c545039f7ae997643c97fd639 (diff) | |
download | chromium_src-4cbf4e5faca5c6d01906d0bbf1092c52f5435d4e.zip chromium_src-4cbf4e5faca5c6d01906d0bbf1092c52f5435d4e.tar.gz chromium_src-4cbf4e5faca5c6d01906d0bbf1092c52f5435d4e.tar.bz2 |
Implement bulk password lookup API in PasswordStoreMac.
Refactor password merge to support the new search.
Rename the fill-targeted search on the adapter for better clarity about the distinction between it and the new search.
BUG=16485
TEST=none yet; UI doesn't exist.
Review URL: http://codereview.chromium.org/149708
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@20877 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser/password_manager')
4 files changed, 241 insertions, 76 deletions
diff --git a/chrome/browser/password_manager/password_store_mac.cc b/chrome/browser/password_manager/password_store_mac.cc index 18b8057..97f9aa0 100644 --- a/chrome/browser/password_manager/password_store_mac.cc +++ b/chrome/browser/password_manager/password_store_mac.cc @@ -161,7 +161,7 @@ void KeychainSearch::FindMatchingItems(std::vector<SecKeychainItemRef>* items) { #pragma mark - // TODO(stuartmorgan): Convert most of this to private helpers in -// MacKeychainPaswordFormAdapter once it has sufficient higher-level public +// MacKeychainPasswordFormAdapter once it has sufficient higher-level public // methods to provide test coverage. namespace internal_keychain_helpers { @@ -369,49 +369,91 @@ PasswordForm* BestKeychainFormForForm( return partial_match; } +// Returns entries from |forms| that are blacklist entries, after removing +// them from |forms|. +std::vector<PasswordForm*> ExtractBlacklistForms( + std::vector<PasswordForm*>* forms) { + std::vector<PasswordForm*> blacklist_forms; + for (std::vector<PasswordForm*>::iterator i = forms->begin(); + i != forms->end();) { + PasswordForm* form = *i; + if (form->blacklisted_by_user) { + blacklist_forms.push_back(form); + i = forms->erase(i); + } else { + ++i; + } + } + return blacklist_forms; +} + +// Deletes and removes from v any element that exists in s. +template <class T> +void DeleteVectorElementsInSet(std::vector<T*>* v, const std::set<T*>& s) { + for (typename std::vector<T*>::iterator i = v->begin(); i != v->end();) { + T* element = *i; + if (s.find(element) != s.end()) { + delete element; + i = v->erase(i); + } else { + ++i; + } + } +} + void MergePasswordForms(std::vector<PasswordForm*>* keychain_forms, std::vector<PasswordForm*>* database_forms, std::vector<PasswordForm*>* merged_forms) { + // Pull out the database blacklist items, since they are used as-is rather + // than being merged with keychain forms. + std::vector<PasswordForm*> database_blacklist_forms = + ExtractBlacklistForms(database_forms); + + // Merge the normal entries. std::set<PasswordForm*> used_keychain_forms; for (std::vector<PasswordForm*>::iterator i = database_forms->begin(); i != database_forms->end();) { PasswordForm* db_form = *i; - bool use_form = false; - if (db_form->blacklisted_by_user) { - // Blacklist entries aren't merged, so just take it directly. - use_form = true; - } else { - // Check for a match in the keychain list. - PasswordForm* best_match = BestKeychainFormForForm(*db_form, - keychain_forms); - if (best_match) { - used_keychain_forms.insert(best_match); - db_form->password_value = best_match->password_value; - use_form = true; - } - } - if (use_form) { + PasswordForm* best_match = BestKeychainFormForForm(*db_form, + keychain_forms); + if (best_match) { + used_keychain_forms.insert(best_match); + db_form->password_value = best_match->password_value; merged_forms->push_back(db_form); i = database_forms->erase(i); } else { ++i; } } - // Find any remaining keychain entries that we want, and clear out everything - // we used. - for (std::vector<PasswordForm*>::iterator i = keychain_forms->begin(); - i != keychain_forms->end();) { - PasswordForm* keychain_form = *i; - if (keychain_form->blacklisted_by_user) { - ++i; + + // Add in the blacklist entries from the database. + merged_forms->insert(merged_forms->end(), + database_blacklist_forms.begin(), + database_blacklist_forms.end()); + + // Clear out all the Keychain entries we used. + DeleteVectorElementsInSet(keychain_forms, used_keychain_forms); +} + +std::vector<PasswordForm*> GetPasswordsForForms( + const MacKeychain& keychain, std::vector<PasswordForm*>* database_forms) { + MacKeychainPasswordFormAdapter keychain_adapter(&keychain); + + std::vector<PasswordForm*> merged_forms; + for (std::vector<PasswordForm*>::iterator i = database_forms->begin(); + i != database_forms->end();) { + std::vector<PasswordForm*> db_form_container(1, *i); + std::vector<PasswordForm*> keychain_matches = + keychain_adapter.PasswordsMergeableWithForm(**i); + MergePasswordForms(&keychain_matches, &db_form_container, &merged_forms); + if (db_form_container.size() == 0) { + i = database_forms->erase(i); } else { - if (used_keychain_forms.find(keychain_form) == used_keychain_forms.end()) - merged_forms->push_back(keychain_form); - else - delete keychain_form; - i = keychain_forms->erase(i); + ++i; } + STLDeleteElements(&keychain_matches); } + return merged_forms; } } // namespace internal_keychain_helpers @@ -419,14 +461,33 @@ void MergePasswordForms(std::vector<PasswordForm*>* keychain_forms, #pragma mark - MacKeychainPasswordFormAdapter::MacKeychainPasswordFormAdapter( - MacKeychain* keychain) : keychain_(keychain), finds_only_owned_(false) { + const MacKeychain* keychain) + : keychain_(keychain), finds_only_owned_(false) { +} + +std::vector<PasswordForm*> + MacKeychainPasswordFormAdapter::PasswordsFillingForm( + const PasswordForm& query_form) { + std::vector<SecKeychainItemRef> keychain_items = + MatchingKeychainItems(query_form.signon_realm, query_form.scheme, + NULL, NULL); + + std::vector<PasswordForm*> keychain_forms = + CreateFormsFromKeychainItems(keychain_items); + for (std::vector<SecKeychainItemRef>::iterator i = keychain_items.begin(); + i != keychain_items.end(); ++i) { + keychain_->Free(*i); + } + return keychain_forms; } std::vector<PasswordForm*> - MacKeychainPasswordFormAdapter::PasswordsMatchingForm( + MacKeychainPasswordFormAdapter::PasswordsMergeableWithForm( const PasswordForm& query_form) { + std::string username = WideToUTF8(query_form.username_value); std::vector<SecKeychainItemRef> keychain_items = - KeychainItemsForFillingForm(query_form); + MatchingKeychainItems(query_form.signon_realm, query_form.scheme, + NULL, username.c_str()); std::vector<PasswordForm*> keychain_forms = CreateFormsFromKeychainItems(keychain_items); @@ -523,12 +584,6 @@ std::vector<PasswordForm*> return keychain_forms; } -std::vector<SecKeychainItemRef> - MacKeychainPasswordFormAdapter::KeychainItemsForFillingForm( - const PasswordForm& form) { - return MatchingKeychainItems(form.signon_realm, form.scheme, NULL, NULL); -} - SecKeychainItemRef MacKeychainPasswordFormAdapter::KeychainItemForForm( const PasswordForm& form) { // We don't store blacklist entries in the keychain, so the answer to "what @@ -702,7 +757,7 @@ void PasswordStoreMac::GetLoginsImpl(GetLoginsRequest* request, const webkit_glue::PasswordForm& form) { MacKeychainPasswordFormAdapter keychain_adapter(keychain_.get()); std::vector<PasswordForm*> keychain_forms = - keychain_adapter.PasswordsMatchingForm(form); + keychain_adapter.PasswordsFillingForm(form); std::vector<PasswordForm*> database_forms; login_metadata_db_->GetLogins(form, &database_forms); @@ -712,24 +767,50 @@ void PasswordStoreMac::GetLoginsImpl(GetLoginsRequest* request, &database_forms, &merged_forms); + // Strip any blacklist entries out of the unused Keychain array, then take + // all the entries that are left (which we can use as imported passwords). + std::vector<PasswordForm*> keychain_blacklist_forms = + internal_keychain_helpers::ExtractBlacklistForms(&keychain_forms); + merged_forms.insert(merged_forms.end(), keychain_forms.begin(), + keychain_forms.end()); + keychain_forms.clear(); + STLDeleteElements(&keychain_blacklist_forms); + // Clean up any orphaned database entries. - for (std::vector<PasswordForm*>::iterator i = database_forms.begin(); - i != database_forms.end(); ++i) { - login_metadata_db_->RemoveLogin(**i); - } - // Delete the forms we aren't returning. + RemoveDatabaseForms(database_forms); STLDeleteElements(&database_forms); - STLDeleteElements(&keychain_forms); NotifyConsumer(request, merged_forms); } void PasswordStoreMac::GetAllLoginsImpl(GetLoginsRequest* request) { - NOTIMPLEMENTED(); + std::vector<PasswordForm*> database_forms; + login_metadata_db_->GetAllLogins(&database_forms, true); + + std::vector<PasswordForm*> merged_forms = + internal_keychain_helpers::GetPasswordsForForms(*keychain_, + &database_forms); + + // Clean up any orphaned database entries. + RemoveDatabaseForms(database_forms); + STLDeleteElements(&database_forms); + + NotifyConsumer(request, merged_forms); } void PasswordStoreMac::GetAllAutofillableLoginsImpl(GetLoginsRequest* request) { - NOTIMPLEMENTED(); + std::vector<PasswordForm*> database_forms; + login_metadata_db_->GetAllLogins(&database_forms, false); + + std::vector<PasswordForm*> merged_forms = + internal_keychain_helpers::GetPasswordsForForms(*keychain_, + &database_forms); + + // Clean up any orphaned database entries. + RemoveDatabaseForms(database_forms); + STLDeleteElements(&database_forms); + + NotifyConsumer(request, merged_forms); } bool PasswordStoreMac::AddToKeychainIfNecessary(const PasswordForm& form) { @@ -756,3 +837,11 @@ bool PasswordStoreMac::DatabaseHasFormMatchingKeychainForm( STLDeleteElements(&database_forms); return has_match; } + +void PasswordStoreMac::RemoveDatabaseForms( + const std::vector<PasswordForm*>& forms) { + for (std::vector<PasswordForm*>::const_iterator i = forms.begin(); + i != forms.end(); ++i) { + login_metadata_db_->RemoveLogin(**i); + } +} diff --git a/chrome/browser/password_manager/password_store_mac.h b/chrome/browser/password_manager/password_store_mac.h index f00fa76..1ea24e9 100644 --- a/chrome/browser/password_manager/password_store_mac.h +++ b/chrome/browser/password_manager/password_store_mac.h @@ -5,6 +5,8 @@ #ifndef CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_STORE_MAC_H_ #define CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_STORE_MAC_H_ +#include <vector> + #include "base/scoped_ptr.h" #include "chrome/browser/password_manager/password_store.h" @@ -37,7 +39,11 @@ class PasswordStoreMac : public PasswordStore { // Returns true if our database contains a form that exactly matches the given // keychain form. bool DatabaseHasFormMatchingKeychainForm( - const webkit_glue::PasswordForm& form); + const webkit_glue::PasswordForm& form); + + // Removes the given forms from the database. + void RemoveDatabaseForms( + const std::vector<webkit_glue::PasswordForm*>& forms); scoped_ptr<MacKeychain> keychain_; scoped_ptr<LoginDatabaseMac> login_metadata_db_; diff --git a/chrome/browser/password_manager/password_store_mac_internal.h b/chrome/browser/password_manager/password_store_mac_internal.h index 50a4088..ba6e07b 100644 --- a/chrome/browser/password_manager/password_store_mac_internal.h +++ b/chrome/browser/password_manager/password_store_mac_internal.h @@ -20,11 +20,17 @@ class MacKeychainPasswordFormAdapter { // Creates an adapter for |keychain|. This class does not take ownership of // |keychain|, so the caller must make sure that the keychain outlives the // created object. - explicit MacKeychainPasswordFormAdapter(MacKeychain* keychain); + explicit MacKeychainPasswordFormAdapter(const MacKeychain* keychain); // Returns PasswordForms for each keychain entry that could be used to fill // |form|. Caller is responsible for deleting the returned forms. - std::vector<webkit_glue::PasswordForm*> PasswordsMatchingForm( + std::vector<webkit_glue::PasswordForm*> PasswordsFillingForm( + const webkit_glue::PasswordForm& query_form); + + // Returns PasswordForms for each keychain entry that could be merged with + // |form|. Differs from PasswordsFillingForm in that the username must match. + // Caller is responsible for deleting the returned forms. + std::vector<webkit_glue::PasswordForm*> PasswordsMergeableWithForm( const webkit_glue::PasswordForm& query_form); // Returns the PasswordForm for the Keychain entry that matches |form| on all @@ -53,12 +59,6 @@ class MacKeychainPasswordFormAdapter { std::vector<webkit_glue::PasswordForm*> CreateFormsFromKeychainItems( const std::vector<SecKeychainItemRef>& items); - // Searches |keychain| for all items usable for the given form, and returns - // them. The caller is responsible for calling MacKeychain::Free on the - // returned items. - std::vector<SecKeychainItemRef> KeychainItemsForFillingForm( - const webkit_glue::PasswordForm& form); - // Searches |keychain| for the specific keychain entry that corresponds to the // given form, and returns it (or NULL if no match is found). The caller is // responsible for calling MacKeychain::Free on on the returned item. @@ -97,7 +97,7 @@ class MacKeychainPasswordFormAdapter { bool SetKeychainItemCreatorCode(const SecKeychainItemRef& keychain_item, OSType creator_code); - MacKeychain* keychain_; + const MacKeychain* keychain_; // If true, Keychain searches are restricted to items created by Chrome. bool finds_only_owned_; @@ -133,15 +133,20 @@ bool FormsMatchForMerge(const webkit_glue::PasswordForm& form_a, // // On return, database_forms and keychain_forms will have only unused // entries; for database_forms that means entries for which no corresponding -// password can be found (and which aren't blacklist entries), but for -// keychain_forms it's only entries we explicitly choose not to use (e.g., -// blacklist entries from other browsers). Keychain entries that we have no -// database matches for will still end up in merged_forms, since they have -// enough information to be used as imported passwords. +// password can be found (and which aren't blacklist entries), and for +// keychain_forms it's entries that weren't merged into at least one database +// form. void MergePasswordForms(std::vector<webkit_glue::PasswordForm*>* keychain_forms, std::vector<webkit_glue::PasswordForm*>* database_forms, std::vector<webkit_glue::PasswordForm*>* merged_forms); +// Fills in the passwords for as many of the forms in |database_forms| as +// possible using entries from |keychain| and returns them. On return, +// |database_forms| will contain only the forms for which no password was found. +std::vector<webkit_glue::PasswordForm*> GetPasswordsForForms( + const MacKeychain& keychain, + std::vector<webkit_glue::PasswordForm*>* database_forms); + } // internal_keychain_helpers #endif // CHROME_BROWSER_PASSWORD_MANAGER_PASSWORD_STORE_MAC_INTERNAL_H_ diff --git a/chrome/browser/password_manager/password_store_mac_unittest.cc b/chrome/browser/password_manager/password_store_mac_unittest.cc index 4962f19..41d5d1e 100644 --- a/chrome/browser/password_manager/password_store_mac_unittest.cc +++ b/chrome/browser/password_manager/password_store_mac_unittest.cc @@ -290,34 +290,49 @@ TEST_F(PasswordStoreMacTest, TestKeychainToFormTranslation) { TEST_F(PasswordStoreMacTest, TestKeychainSearch) { struct TestDataAndExpectation { const PasswordFormData data; - const size_t expected_matches; + const size_t expected_fill_matches; + const size_t expected_merge_matches; }; // Most fields are left blank because we don't care about them for searching. TestDataAndExpectation test_data[] = { // An HTML form we've seen. { { PasswordForm::SCHEME_HTML, "http://some.domain.com/", - NULL, NULL, NULL, NULL, NULL, NULL, NULL, false, false, 0 }, 2 }, + NULL, NULL, NULL, NULL, NULL, L"joe_user", NULL, false, false, 0 }, + 2, 2 }, + { { PasswordForm::SCHEME_HTML, "http://some.domain.com/", + NULL, NULL, NULL, NULL, NULL, L"wrong_user", NULL, false, false, 0 }, + 2, 0 }, // An HTML form we haven't seen { { PasswordForm::SCHEME_HTML, "http://www.unseendomain.com/", - NULL, NULL, NULL, NULL, NULL, NULL, NULL, false, false, 0 }, 0 }, + NULL, NULL, NULL, NULL, NULL, L"joe_user", NULL, false, false, 0 }, + 0, 0 }, // Basic auth that should match. { { PasswordForm::SCHEME_BASIC, "http://some.domain.com:4567/low_security", - NULL, NULL, NULL, NULL, NULL, NULL, NULL, false, false, 0 }, 1 }, + NULL, NULL, NULL, NULL, NULL, L"basic_auth_user", NULL, false, false, + 0 }, + 1, 1 }, // Basic auth with the wrong port. { { PasswordForm::SCHEME_BASIC, "http://some.domain.com:1111/low_security", - NULL, NULL, NULL, NULL, NULL, NULL, NULL, false, false, 0 }, 0 }, + NULL, NULL, NULL, NULL, NULL, L"basic_auth_user", NULL, false, false, + 0 }, + 0, 0 }, // Digest auth we've saved under https, visited with http. { { PasswordForm::SCHEME_DIGEST, "http://some.domain.com/high_security", - NULL, NULL, NULL, NULL, NULL, NULL, NULL, false, false, 0 }, 0 }, + NULL, NULL, NULL, NULL, NULL, L"digest_auth_user", NULL, false, false, + 0 }, + 0, 0 }, // Digest auth that should match. { { PasswordForm::SCHEME_DIGEST, "https://some.domain.com/high_security", - NULL, NULL, NULL, NULL, NULL, NULL, NULL, false, true, 0 }, 1 }, + NULL, NULL, NULL, NULL, NULL, L"wrong_user", NULL, false, true, 0 }, + 1, 0 }, // Digest auth with the wrong domain. { { PasswordForm::SCHEME_DIGEST, "https://some.domain.com/other_domain", - NULL, NULL, NULL, NULL, NULL, NULL, NULL, false, true, 0 }, 0 }, + NULL, NULL, NULL, NULL, NULL, L"digest_auth_user", NULL, false, true, + 0 }, + 0, 0 }, // Garbage forms should have no matches. { { PasswordForm::SCHEME_HTML, "foo/bar/baz", - NULL, NULL, NULL, NULL, NULL, NULL, NULL, false, false, 0 }, 0 }, + NULL, NULL, NULL, NULL, NULL, NULL, NULL, false, false, 0 }, 0, 0 }, }; MacKeychainPasswordFormAdapter keychain_adapter(keychain_); @@ -326,14 +341,21 @@ TEST_F(PasswordStoreMacTest, TestKeychainSearch) { for (unsigned int i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { scoped_ptr<PasswordForm> query_form( CreatePasswordFormFromData(test_data[i].data)); + + // Check matches treating the form as a fill target. std::vector<PasswordForm*> matching_items = - keychain_adapter.PasswordsMatchingForm(*query_form); - EXPECT_EQ(test_data[i].expected_matches, matching_items.size()); + keychain_adapter.PasswordsFillingForm(*query_form); + EXPECT_EQ(test_data[i].expected_fill_matches, matching_items.size()); + STLDeleteElements(&matching_items); + + // Check matches teating the form as a merging target. + matching_items = keychain_adapter.PasswordsMergeableWithForm(*query_form); + EXPECT_EQ(test_data[i].expected_merge_matches, matching_items.size()); STLDeleteElements(&matching_items); // None of the pre-seeded items are owned by us, so none should match an // owned-passwords-only search. - matching_items = owned_keychain_adapter.PasswordsMatchingForm(*query_form); + matching_items = owned_keychain_adapter.PasswordsFillingForm(*query_form); EXPECT_EQ(0U, matching_items.size()); STLDeleteElements(&matching_items); } @@ -522,7 +544,7 @@ TEST_F(PasswordStoreMacTest, TestKeychainRemove) { PasswordForm* add_form = CreatePasswordFormFromData(test_data[0].data); EXPECT_TRUE(owned_keychain_adapter.AddPassword(*add_form)); delete add_form; - + for (unsigned int i = 0; i < ARRAYSIZE_UNSAFE(test_data); ++i) { scoped_ptr<PasswordForm> form(CreatePasswordFormFromData( test_data[i].data)); @@ -687,7 +709,7 @@ TEST_F(PasswordStoreMacTest, TestFormMerge) { test_data[DATABASE_INPUT][current_test].push_back(&db_user_3_with_path); test_data[MERGE_OUTPUT][current_test].push_back(&merged_user_1); test_data[MERGE_OUTPUT][current_test].push_back(&merged_user_1_with_db_path); - test_data[MERGE_OUTPUT][current_test].push_back(&keychain_user_2); + test_data[KEYCHAIN_OUTPUT][current_test].push_back(&keychain_user_2); test_data[DATABASE_OUTPUT][current_test].push_back(&db_user_3_with_path); // Test a merge where Chrome has a blacklist entry, and the keychain has @@ -701,7 +723,7 @@ TEST_F(PasswordStoreMacTest, TestFormMerge) { // subpath, and we want access to the password on other paths. test_data[MERGE_OUTPUT][current_test].push_back( &database_blacklist_with_path); - test_data[MERGE_OUTPUT][current_test].push_back(&keychain_user_1); + test_data[KEYCHAIN_OUTPUT][current_test].push_back(&keychain_user_1); // Test a merge where Chrome has an account, and Keychain has a blacklist // (from another browser) and the Chrome password data. @@ -754,3 +776,46 @@ TEST_F(PasswordStoreMacTest, TestFormMerge) { STLDeleteElements(&merged_forms); } } + +TEST_F(PasswordStoreMacTest, TestPasswordBulkLookup) { + PasswordFormData db_data[] = { + { PasswordForm::SCHEME_HTML, "http://some.domain.com/", + "http://some.domain.com/", "http://some.domain.com/action.cgi", + L"submit", L"username", L"password", L"joe_user", L"", + true, false, 1212121212 }, + { PasswordForm::SCHEME_HTML, "http://some.domain.com/", + "http://some.domain.com/page.html", + "http://some.domain.com/handlepage.cgi", + L"submit", L"username", L"password", L"joe_user", L"", + true, false, 1234567890 }, + { PasswordForm::SCHEME_HTML, "http://some.domain.com/", + "http://some.domain.com/page.html", + "http://some.domain.com/handlepage.cgi", + L"submit", L"username", L"password", L"second-account", L"", + true, false, 1240000000 }, + { PasswordForm::SCHEME_HTML, "http://dont.remember.com/", + "http://dont.remember.com/", + "http://dont.remember.com/handlepage.cgi", + L"submit", L"username", L"password", L"joe_user", L"", + true, false, 1240000000 }, + { PasswordForm::SCHEME_HTML, "http://some.domain.com/", + "http://some.domain.com/path.html", "http://some.domain.com/action.cgi", + L"submit", L"username", L"password", NULL, NULL, + true, false, 1212121212 }, + }; + std::vector<PasswordForm*> database_forms; + for (unsigned int i = 0; i < ARRAYSIZE_UNSAFE(db_data); ++i) { + database_forms.push_back(CreatePasswordFormFromData(db_data[i])); + } + std::vector<PasswordForm*> merged_forms = + internal_keychain_helpers::GetPasswordsForForms(*keychain_, + &database_forms); + EXPECT_EQ(2U, database_forms.size()); + ASSERT_EQ(3U, merged_forms.size()); + EXPECT_EQ(std::wstring(L"sekrit"), merged_forms[0]->password_value); + EXPECT_EQ(std::wstring(L"sekrit"), merged_forms[1]->password_value); + EXPECT_EQ(true, merged_forms[2]->blacklisted_by_user); + + STLDeleteElements(&database_forms); + STLDeleteElements(&merged_forms); +} |