| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/omnibox/browser/enterprise_search_aggregator_provider.h" |
| |
| #include <iterator> |
| #include <memory> |
| #include <optional> |
| #include <ranges> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/contains.h" |
| #include "base/json/json_reader.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/scoped_refptr.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/icu_test_util.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_future.h" |
| #include "base/time/time.h" |
| #include "components/omnibox/browser/autocomplete_enums.h" |
| #include "components/omnibox/browser/autocomplete_input.h" |
| #include "components/omnibox/browser/autocomplete_match.h" |
| #include "components/omnibox/browser/autocomplete_match_type.h" |
| #include "components/omnibox/browser/autocomplete_provider_listener.h" |
| #include "components/omnibox/browser/fake_autocomplete_provider_client.h" |
| #include "components/omnibox/browser/test_scheme_classifier.h" |
| #include "components/omnibox/common/omnibox_feature_configs.h" |
| #include "components/search_engines/template_url.h" |
| #include "components/search_engines/template_url_data.h" |
| #include "components/search_engines/template_url_service.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "third_party/metrics_proto/omnibox_event.pb.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/page_transition_types.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| using testing::_; |
| using testing::FieldsAre; |
| using testing::Return; |
| using SuggestionType = AutocompleteMatch::EnterpriseSearchAggregatorType; |
| |
| // Concats optional strings to generate sliced and unsliced histogram names. |
| // E.g. X, X.Y1, X.Y2.Z. |
| std::string HistogramName(const std::string& base, |
| std::optional<std::string> type, |
| std::optional<bool> completed) { |
| std::string name = base; |
| if (type.has_value()) |
| name += "." + type.value(); |
| if (completed.has_value()) |
| name += completed.value() ? ".Completed" : ".Interrupted"; |
| return name; |
| } |
| |
| // Helper to cleanly verify logging. Intentionally verifies all histograms; i.e. |
| // doesn't allow verifying 1 histogram is correct and not caring whether another |
| // histogram is logged. The default constructor `{}` expects no histogram to be |
| // logged. Can use either the constructor `{.unsliced_completed = true}` or |
| // direct assignment `expected_histogram.unsliced_completed = true` to set |
| // expectations. |
| // |
| // E.g.: |
| // ExpectedHistogram expected_histogram{.query_completed = true}; |
| // // Complete query request. |
| // expected_histogram.Verify(); |
| // |
| // // Interrupt provider. |
| // expected_histogram.unsliced_interrupted = true; |
| // expected_histogram.people_interrupted = true; |
| // // ~ExpectedHistogram() will verify expectations. |
| |
| struct ExpectedHistogram { |
| ~ExpectedHistogram() { Verify(); } |
| |
| void Verify() { |
| std::string kTimeHistogramName = |
| "Omnibox.SuggestRequestsSent.ResponseTime2.RequestState." |
| "EnterpriseSearchAggregatorSuggest"; |
| std::string kCountHistogramName = |
| "Omnibox.SuggestRequestsSent.ResultCount." |
| "EnterpriseSearchAggregatorSuggest"; |
| |
| // Verify timing histograms. |
| histogram_tester.ExpectTotalCount( |
| HistogramName(kTimeHistogramName, {}, {}), |
| unsliced_completed + unsliced_interrupted); |
| histogram_tester.ExpectTotalCount( |
| HistogramName(kTimeHistogramName, {}, true), unsliced_completed); |
| histogram_tester.ExpectTotalCount( |
| HistogramName(kTimeHistogramName, {}, false), unsliced_interrupted); |
| histogram_tester.ExpectTotalCount( |
| HistogramName(kTimeHistogramName, "Query", {}), |
| query_completed + query_interrupted); |
| histogram_tester.ExpectTotalCount( |
| HistogramName(kTimeHistogramName, "Query", true), query_completed); |
| histogram_tester.ExpectTotalCount( |
| HistogramName(kTimeHistogramName, "Query", false), query_interrupted); |
| histogram_tester.ExpectTotalCount( |
| HistogramName(kTimeHistogramName, "People", {}), |
| people_completed + people_interrupted); |
| histogram_tester.ExpectTotalCount( |
| HistogramName(kTimeHistogramName, "People", true), people_completed); |
| histogram_tester.ExpectTotalCount( |
| HistogramName(kTimeHistogramName, "People", false), people_interrupted); |
| histogram_tester.ExpectTotalCount( |
| HistogramName(kTimeHistogramName, "Content", {}), |
| content_completed + content_interrupted); |
| histogram_tester.ExpectTotalCount( |
| HistogramName(kTimeHistogramName, "Content", true), content_completed); |
| histogram_tester.ExpectTotalCount( |
| HistogramName(kTimeHistogramName, "Content", false), |
| content_interrupted); |
| |
| // Verify count histograms. |
| EXPECT_THAT( |
| histogram_tester.GetAllSamples( |
| HistogramName(kCountHistogramName, {}, {})), |
| testing::ElementsAreArray(unsliced_count.has_value() |
| ? std::vector<base::Bucket>{base::Bucket( |
| unsliced_count.value(), 1)} |
| : std::vector<base::Bucket>{})); |
| EXPECT_THAT( |
| histogram_tester.GetAllSamples( |
| HistogramName(kCountHistogramName, "Query", {})), |
| testing::ElementsAreArray(query_count.has_value() |
| ? std::vector<base::Bucket>{base::Bucket( |
| query_count.value(), 1)} |
| : std::vector<base::Bucket>{})); |
| EXPECT_THAT( |
| histogram_tester.GetAllSamples( |
| HistogramName(kCountHistogramName, "People", {})), |
| testing::ElementsAreArray(people_count.has_value() |
| ? std::vector<base::Bucket>{base::Bucket( |
| people_count.value(), 1)} |
| : std::vector<base::Bucket>{})); |
| EXPECT_THAT( |
| histogram_tester.GetAllSamples( |
| HistogramName(kCountHistogramName, "Content", {})), |
| testing::ElementsAreArray(content_count.has_value() |
| ? std::vector<base::Bucket>{base::Bucket( |
| content_count.value(), 1)} |
| : std::vector<base::Bucket>{})); |
| } |
| |
| base::HistogramTester histogram_tester; |
| |
| // Timing histograms. If false, expected to not be logged. If true, expected |
| // to log exactly once. |
| bool unsliced_completed = false; |
| bool unsliced_interrupted = false; |
| bool query_completed = false; |
| bool query_interrupted = false; |
| bool people_completed = false; |
| bool people_interrupted = false; |
| bool content_completed = false; |
| bool content_interrupted = false; |
| |
| // Count histograms. If `nullopt`, expected to not be logged. If not null, |
| // even if 0, expected to log its value exactly once. |
| std::optional<int> unsliced_count; |
| std::optional<int> query_count; |
| std::optional<int> people_count; |
| std::optional<int> content_count; |
| }; |
| } // namespace |
| |
| class FakeEnterpriseSearchAggregatorProvider |
| : public EnterpriseSearchAggregatorProvider { |
| public: |
| FakeEnterpriseSearchAggregatorProvider(AutocompleteProviderClient* client, |
| AutocompleteProviderListener* listener) |
| : EnterpriseSearchAggregatorProvider(client, listener) {} |
| using EnterpriseSearchAggregatorProvider::AggregateMatches; |
| using EnterpriseSearchAggregatorProvider::CreateMatch; |
| using EnterpriseSearchAggregatorProvider::EnterpriseSearchAggregatorProvider; |
| using EnterpriseSearchAggregatorProvider::IsProviderAllowed; |
| using EnterpriseSearchAggregatorProvider::RequestCompleted; |
| using EnterpriseSearchAggregatorProvider::RequestStarted; |
| |
| using EnterpriseSearchAggregatorProvider::adjusted_input_; |
| using EnterpriseSearchAggregatorProvider::done_; |
| using EnterpriseSearchAggregatorProvider::matches_; |
| using EnterpriseSearchAggregatorProvider::requests_; |
| using EnterpriseSearchAggregatorProvider::template_url_; |
| |
| protected: |
| ~FakeEnterpriseSearchAggregatorProvider() override = default; |
| }; |
| |
| const std::string kGoodJsonResponse = base::StringPrintf( |
| R"({ |
| "querySuggestions": [ |
| { |
| "suggestion": "John's Document 1", |
| "dataStore": [] |
| } |
| ], |
| "peopleSuggestions": [ |
| { |
| "suggestion": "john@example.com", |
| "document": { |
| "name": "sundar", |
| "derivedStructData": { |
| "name": { |
| "display_name_lower": "john doe", |
| "familyName": "Doe", |
| "givenName": "John", |
| "given_name_lower": "john", |
| "family_name_lower": "doe", |
| "displayName": "John Doe", |
| "userName": "john" |
| }, |
| "emails": [ |
| { |
| "type": "primary", |
| "value": "john@example.com" |
| } |
| ], |
| "displayPhoto": { |
| "url": "https://example.com/image.png" |
| } |
| } |
| }, |
| "destinationUri": "https://example.com/people/jdoe", |
| "score": 0.8, |
| "dataStore": "project 1" |
| } |
| ], |
| "contentSuggestions": [ |
| { |
| "suggestion": "John's doodle", |
| "contentType": "THIRD_PARTY", |
| "document": { |
| "name": "Document 2", |
| "derivedStructData": { |
| "source_type": "jira", |
| "entity_type": "issue", |
| "title": "John's doodle", |
| "link": "https://www.example.co.uk", |
| "owner": "John Doe", |
| "mime_type": "application/vnd.google-apps.document", |
| "updated_time": 1192487100 |
| } |
| }, |
| "destinationUri": "https://www.example.com", |
| "iconUri": "https://example.com/icon.png", |
| "score": 0.4, |
| "dataStore": "project2" |
| } |
| ] |
| })"); |
| |
| const std::string kQueryGoodJsonResponse = R"({ |
| "querySuggestions": [ |
| { |
| "suggestion": "John's Document 1", |
| "dataStore": [] |
| } |
| ] |
| })"; |
| |
| const std::string kPeopleGoodJsonResponse = R"({ |
| "peopleSuggestions": [ |
| { |
| "suggestion": "john@example.com", |
| "document": { |
| "name": "sundar", |
| "derivedStructData": { |
| "name": { |
| "display_name_lower": "john doe", |
| "familyName": "Doe", |
| "givenName": "John", |
| "given_name_lower": "john", |
| "family_name_lower": "doe", |
| "displayName": "John Doe", |
| "userName": "john" |
| }, |
| "emails": [ |
| { |
| "type": "primary", |
| "value": "john@example.com" |
| } |
| ], |
| "displayPhoto": { |
| "url": "https://example.com/image.png" |
| } |
| } |
| }, |
| "destinationUri": "https://example.com/people/jdoe", |
| "score": 0.8, |
| "dataStore": "project 1" |
| } |
| ] |
| })"; |
| |
| const std::string kContentGoodJsonResponse = R"({ |
| "contentSuggestions": [ |
| { |
| "suggestion": "John's doodle", |
| "contentType": "THIRD_PARTY", |
| "document": { |
| "name": "Document 2", |
| "derivedStructData": { |
| "source_type": "jira", |
| "entity_type": "issue", |
| "title": "John's doodle", |
| "link": "https://www.example.co.uk", |
| "owner": "John Doe", |
| "mime_type": "application/vnd.google-apps.document", |
| "updated_time": 1192487100 |
| } |
| }, |
| "destinationUri": "https://www.example.com", |
| "iconUri": "https://example.com/icon.png", |
| "score": 0.4, |
| "dataStore": "project2" |
| } |
| ] |
| })"; |
| |
| const std::string kGoodEmptyJsonResponse = base::StringPrintf( |
| R"({ |
| "querySuggestions": [], |
| "peopleSuggestions": [], |
| "contentSuggestions": [] |
| })"); |
| |
| const std::string kMissingFieldsJsonResponse = base::StringPrintf( |
| R"({ |
| "querySuggestions": [ |
| { |
| "suggestion": "", |
| "dataStore": [] |
| }, |
| { |
| "suggestion": "John's Document 1", |
| "dataStore": [] |
| } |
| ], |
| "peopleSuggestions": [ |
| { |
| "suggestion": "missingDisplayName@example.com", |
| "document": { |
| "derivedStructData": { |
| "name": { |
| "userName": "missingDisplayName" |
| } |
| } |
| }, |
| "destinationUri": "https://example.com/people/jdoe" |
| }, |
| { |
| "suggestion": "missingUri@example.com", |
| "document": { |
| "derivedStructData": { |
| "name": { |
| "displayName": "John Doe", |
| "userName": "john" |
| } |
| } |
| } |
| }, |
| { |
| "document": { |
| "derivedStructData": { |
| "name": { |
| "displayName": "Missing suggestion / user name / URI" |
| } |
| } |
| } |
| }, |
| { |
| "suggestion": "john@example.com", |
| "document": { |
| "derivedStructData": { |
| "name": { |
| "displayName": "John Doe", |
| "userName": "john" |
| } |
| } |
| }, |
| "destinationUri": "https://example.com/people/jdoe" |
| } |
| ], |
| "contentSuggestions": [ |
| { |
| "document": { |
| "derivedStructData": { |
| "title": "Missing URI" |
| } |
| } |
| }, |
| { |
| "document": { |
| "name": "Document 2", |
| "derivedStructData": { |
| "link": "https://www.missingTitle.co.uk" |
| } |
| }, |
| "destinationUri": "https://www.missingTitle.com" |
| }, |
| { |
| "document": { |
| "name": "Document 2", |
| "derivedStructData": { |
| "title": "John's doodle'", |
| "link": "https://www.missinguributlinkavailable.co.uk" |
| } |
| } |
| }, |
| { |
| "document": { |
| "name": "Document 3", |
| "derivedStructData": { |
| "title": "John's doodle'", |
| "link": "https://www.example.co.uk" |
| } |
| }, |
| "destinationUri": "https://www.example.com" |
| } |
| ] |
| })"); |
| |
| const std::string kGoodJsonResponseImageUrls = base::StringPrintf( |
| R"({ |
| "querySuggestions": [], |
| "peopleSuggestions": [ |
| { |
| "suggestion": "john@example.com", |
| "document": { |
| "name": "sundar", |
| "derivedStructData": { |
| "name": { |
| "familyName": "Doe", |
| "givenName": "John", |
| "displayName": "John Doe" |
| }, |
| "emails": [ |
| { |
| "type": "primary", |
| "value": "john@example.com" |
| } |
| ], |
| "displayPhoto": { |
| "url": "https://lh3.googleusercontent.com/some/path-s100" |
| } |
| } |
| }, |
| "destinationUri": "https://example.com/people/jdoe", |
| "dataStore": "project 1" |
| }, |
| { |
| "suggestion": "john2@example.com", |
| "document": { |
| "name": "sundar2", |
| "derivedStructData": { |
| "name": { |
| "familyName": "Doe2", |
| "givenName": "John", |
| "displayName": "John Doe2" |
| }, |
| "emails": [ |
| { |
| "type": "primary", |
| "value": "john2@example.com" |
| } |
| ], |
| "displayPhoto": { |
| "url": "https://lh3.googleusercontent.com/some/path=s100" |
| } |
| } |
| }, |
| "destinationUri": "https://example.com/people/jdoe2", |
| "dataStore": "project 1" |
| }, |
| { |
| "suggestion": "john3@example.com", |
| "document": { |
| "name": "sundar3", |
| "derivedStructData": { |
| "name": { |
| "familyName": "Doe3", |
| "givenName": "John", |
| "displayName": "John Doe3" |
| }, |
| "emails": [ |
| { |
| "type": "primary", |
| "value": "john3@example.com" |
| } |
| ], |
| "displayPhoto": { |
| "url": "https://lh3.googleusercontent.com/some/path=abc" |
| } |
| } |
| }, |
| "destinationUri": "https://example.com/people/jdoe3", |
| "dataStore": "project 1" |
| }, |
| { |
| "suggestion": "john4@example.com", |
| "document": { |
| "name": "sundar4", |
| "derivedStructData": { |
| "name": { |
| "familyName": "Doe4", |
| "givenName": "John", |
| "displayName": "John Doe4" |
| }, |
| "emails": [ |
| { |
| "type": "primary", |
| "value": "john4@example.com" |
| } |
| ], |
| "displayPhoto": { |
| "url": "https://lh3.googleusercontent.com/some/path=w100-h200" |
| } |
| } |
| }, |
| "destinationUri": "https://example.com/people/jdoe4", |
| "dataStore": "project 1" |
| } |
| ], |
| "contentSuggestions": [] |
| })"); |
| |
| const std::string kNonDictJsonResponse = |
| base::StringPrintf(R"(["test","result1","result2"])"); |
| |
| // Helper methods to dynamically generate valid responses. |
| std::string CreateQueryResult(const std::string& query, |
| const float score = 0.0) { |
| return base::StringPrintf( |
| R"( |
| {"suggestion": "%s", "score": %0.1f} |
| )", |
| query, score); |
| } |
| std::string CreatePeopleResult(const std::string& displayName, |
| const std::string& userName, |
| const std::string& givenName, |
| const std::string& familyName, |
| const float score = 0.0) { |
| return base::StringPrintf( |
| R"( |
| { |
| "suggestion": "%s@example.com", |
| "document": { |
| "derivedStructData": { |
| "name": { |
| "displayName": "%s", |
| "givenName": "%s", |
| "familyName": "%s" |
| }, |
| "emails": [ |
| { |
| "type": "", |
| "value": "%s@example.com" |
| } |
| ] |
| } |
| }, |
| "destinationUri": "https://example.com/people/%s", |
| "score": %0.1f |
| } |
| )", |
| userName, displayName, givenName, familyName, userName, userName, score); |
| } |
| std::string CreatePeopleResultWithoutEmailField(const std::string& displayName, |
| const std::string& userName, |
| const std::string& givenName, |
| const std::string& familyName, |
| const float score = 0.0) { |
| return base::StringPrintf( |
| R"( |
| { |
| "suggestion": "%s", |
| "document": { |
| "derivedStructData": { |
| "name": { |
| "displayName": "%s", |
| "givenName": "%s", |
| "familyName": "%s" |
| } |
| } |
| }, |
| "destinationUri": "https://example.com/people/%s", |
| "score": %0.1f |
| } |
| )", |
| displayName, displayName, givenName, familyName, userName, score); |
| } |
| std::string CreateContentResult(const std::string& title, |
| const std::string& url, |
| const float score = 0.0) { |
| return base::StringPrintf( |
| R"( |
| { |
| "document": { |
| "derivedStructData": { |
| "title": "%s" |
| } |
| }, |
| "destinationUri": "%s", |
| "score": %0.1f |
| } |
| )", |
| title, url, score); |
| } |
| |
| std::string CreateContentResultWithOwnerEmail(const std::string& title, |
| const std::string& owner_email, |
| const std::string& url) { |
| return base::StringPrintf( |
| R"( |
| { |
| "document": { |
| "derivedStructData": { |
| "title": "%s", |
| "owner_email": "%s" |
| } |
| }, |
| "destinationUri": "%s" |
| } |
| )", |
| title, owner_email, url); |
| } |
| std::string CreateContentResultWithTypes(const std::string& title, |
| const std::string& url, |
| const std::string& mime_type, |
| const std::string& source_type) { |
| return base::StringPrintf( |
| R"( |
| { |
| "document": { |
| "derivedStructData": { |
| "title": "%s", |
| "mime_type": "%s", |
| "source_type": "%s" |
| } |
| }, |
| "destinationUri": "%s" |
| } |
| )", |
| title, mime_type, source_type, url); |
| } |
| std::string CreateResponse(std::vector<std::string> queries, |
| std::vector<std::string> peoples, |
| std::vector<std::string> contents) { |
| auto jointQueries = base::JoinString(queries, ","); |
| auto jointPeoples = base::JoinString(peoples, ","); |
| auto jointContents = base::JoinString(contents, ","); |
| return base::StringPrintf( |
| R"( |
| { |
| "querySuggestions": [%s], |
| "peopleSuggestions": [%s], |
| "contentSuggestions": [%s] |
| } |
| )", |
| jointQueries, jointPeoples, jointContents); |
| } |
| |
| AutocompleteInput CreateInput(const std::u16string& text, |
| bool in_keyword_mode) { |
| AutocompleteInput input = {text, metrics::OmniboxEventProto::OTHER, |
| TestSchemeClassifier()}; |
| if (in_keyword_mode) { |
| input.set_keyword_mode_entry_method(metrics::OmniboxEventProto::TAB); |
| } |
| return input; |
| } |
| |
| class MockAutocompleteProviderListener : public AutocompleteProviderListener { |
| public: |
| MOCK_METHOD(void, |
| OnProviderUpdate, |
| (bool updated_matches, const AutocompleteProvider* provider), |
| (override)); |
| }; |
| |
| class EnterpriseSearchAggregatorProviderTest : public testing::Test { |
| protected: |
| void SetUp() override { |
| InitFeature(); |
| client_ = std::make_unique<FakeAutocompleteProviderClient>(); |
| InitClient(); |
| mock_listener_ = std::make_unique< |
| testing::StrictMock<MockAutocompleteProviderListener>>(); |
| provider_ = new FakeEnterpriseSearchAggregatorProvider( |
| client_.get(), mock_listener_.get()); |
| InitTemplateUrlService(); |
| } |
| |
| void InitClient() { |
| EXPECT_CALL(*client_.get(), IsAuthenticated()).WillRepeatedly(Return(true)); |
| EXPECT_CALL(*client_.get(), IsOffTheRecord()).WillRepeatedly(Return(false)); |
| EXPECT_CALL(*client_.get(), SearchSuggestEnabled()) |
| .WillRepeatedly(Return(true)); |
| } |
| |
| virtual void InitFeature() { |
| scoped_config_.Get().enabled = true; |
| scoped_config_.Get().relevance_scoring_mode = "client"; |
| } |
| |
| void InitTemplateUrlService() { |
| TemplateURLData data; |
| data.SetShortName(u"keyword"); |
| data.SetKeyword(u"keyword"); |
| data.SetURL("https://www.google.com/?q={searchTerms}"); |
| data.suggestions_url = "https://www.google.com/complete/?q={searchTerms}"; |
| data.favicon_url = GURL("https://www.google.com/favicon.ico"); |
| data.is_active = TemplateURLData::ActiveStatus::kTrue; |
| data.featured_by_policy = true; |
| data.policy_origin = TemplateURLData::PolicyOrigin::kSearchAggregator; |
| provider_->template_url_ = client_->GetTemplateURLService()->Add( |
| std::make_unique<TemplateURL>(data)); |
| } |
| |
| AutocompleteMatch CreateAutocompleteMatch(std::u16string url) { |
| AutocompleteMatch match(provider_.get(), 1000, false, |
| AutocompleteMatchType::NAVSUGGEST); |
| match.destination_url = GURL(url); |
| return match; |
| } |
| |
| // Returns `destination_url`s of `matches_`. |
| std::vector<std::u16string> GetMatches() { |
| std::vector<std::u16string> matches; |
| for (const auto& m : provider_->matches_) |
| matches.push_back(base::UTF8ToUTF16(m.destination_url.spec())); |
| return matches; |
| } |
| |
| // Returns `destination_url`s and `relevance`s of `matches_`. |
| using ScoredMatch = std::pair<std::u16string, int>; |
| std::vector<ScoredMatch> GetScoredMatches() { |
| std::vector<ScoredMatch> matches; |
| for (const auto& m : provider_->matches_) |
| matches.emplace_back(base::UTF8ToUTF16(m.destination_url.spec()), |
| m.relevance); |
| return matches; |
| } |
| |
| // Simulate requests starting or resetting. Requests whose indexes are |
| // included in `allowed_requests` are expected to start and completed or be |
| // interrupted. Requests excluded will be reset but not expected to start, |
| // complete, or be interrupted. This mirrors the lifecycle of real `Request`s. |
| // This acts like calling `Run()` without kicking off the remote service. Does |
| // not simulate `Start()`; i.e. skips over provider eligibility checks. |
| void StartRequests(std::vector<int> allowed_requests) { |
| provider_->done_ = false; |
| |
| for (size_t i = 0; i < provider_->requests_.size(); ++i) { |
| bool allowed = base::Contains(allowed_requests, i); |
| provider_->requests_[i].Reset(!allowed); |
| if (allowed) { |
| provider_->RequestStarted(i, nullptr); |
| } |
| } |
| |
| EXPECT_CALL(*mock_listener_.get(), OnProviderUpdate(_, provider_.get())) |
| .Times(1); |
| provider_->AggregateMatches(); |
| } |
| |
| // Simulate a request completing either successfully, with error, or empty. |
| void CompleteRequest(int index, int code, std::string response) { |
| base::test::TestFuture<void> future_; |
| EXPECT_CALL(*mock_listener_.get(), OnProviderUpdate(_, provider_.get())) |
| .Times(1) |
| .WillOnce([&] { future_.SetValue(); }); |
| provider_->RequestCompleted(index, nullptr, code, |
| std::make_unique<std::string>(response)); |
| EXPECT_TRUE(future_.WaitAndClear()); |
| testing::Mock::VerifyAndClearExpectations(mock_listener_.get()); |
| } |
| |
| // Helper to combine `StartRequests()` and `CompleteRequest()`. |
| void StartAndCompleteRequests( |
| int response_code, |
| std::vector<std::optional<std::string>> response_strings) { |
| std::vector<int> allowed_requests; |
| for (size_t i = 0; i < response_strings.size(); ++i) { |
| if (response_strings[i].has_value()) { |
| allowed_requests.push_back(i); |
| } |
| } |
| |
| StartRequests(allowed_requests); |
| |
| for (auto i : allowed_requests) { |
| CompleteRequest(i, response_code, *response_strings[i]); |
| } |
| |
| EXPECT_TRUE(provider_->done_); |
| } |
| |
| // Helper to start and complete all 3 requests with the same response. |
| void StartAndComplete3Requests(int response_code, |
| const std::string& response_string) { |
| StartAndCompleteRequests( |
| response_code, |
| {{response_string}, {response_string}, {response_string}}); |
| } |
| |
| base::test::SingleThreadTaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| std::unique_ptr<FakeAutocompleteProviderClient> client_; |
| std::unique_ptr<testing::StrictMock<MockAutocompleteProviderListener>> |
| mock_listener_; |
| scoped_refptr<FakeEnterpriseSearchAggregatorProvider> provider_; |
| omnibox_feature_configs::ScopedConfigForTesting< |
| omnibox_feature_configs::SearchAggregatorProvider> |
| scoped_config_; |
| }; |
| |
| // `multiple_requests` is used in the provider's constructor, so it has to be |
| // setup before the provider is constructed. |
| class EnterpriseSearchAggregatorProviderSingleRequestTest |
| : public EnterpriseSearchAggregatorProviderTest { |
| void InitFeature() override { |
| EnterpriseSearchAggregatorProviderTest::InitFeature(); |
| scoped_config_.Get().multiple_requests = false; |
| } |
| }; |
| |
| TEST_F(EnterpriseSearchAggregatorProviderTest, CreateMatch) { |
| provider_->adjusted_input_ = CreateInput(u"input", true); |
| auto primary_text_class = [&](auto suggestion_type) { |
| return suggestion_type == |
| AutocompleteMatch::EnterpriseSearchAggregatorType::PEOPLE |
| ? std::vector< |
| ACMatchClassification>{{0, ACMatchClassification::NONE}} |
| : std::vector<ACMatchClassification>{ |
| {0, ACMatchClassification::MATCH}, |
| {5, ACMatchClassification::NONE}}; |
| }; |
| auto secondary_text_class = [&](auto suggestion_type) { |
| return std::vector<ACMatchClassification>{{0, ACMatchClassification::DIM}}; |
| }; |
| |
| auto query_match = provider_->CreateMatch( |
| AutocompleteMatch::EnterpriseSearchAggregatorType::QUERY, false, {1000}, |
| "https://url.com/", "https://example.com/image.png", |
| "https://example.com/icon.png", u"additional text", u"input title", |
| u"keyword input title"); |
| EXPECT_EQ(query_match.relevance, 1000); |
| EXPECT_EQ(query_match.destination_url.spec(), "https://url.com/"); |
| EXPECT_EQ(query_match.fill_into_edit, u"keyword input title"); |
| EXPECT_EQ(query_match.enterprise_search_aggregator_type, |
| AutocompleteMatch::EnterpriseSearchAggregatorType::QUERY); |
| EXPECT_EQ(query_match.description, u"additional text"); |
| EXPECT_EQ(query_match.description_class, |
| secondary_text_class( |
| AutocompleteMatch::EnterpriseSearchAggregatorType::QUERY)); |
| EXPECT_EQ(query_match.contents, u"input title"); |
| EXPECT_EQ(query_match.contents_class, |
| primary_text_class( |
| AutocompleteMatch::EnterpriseSearchAggregatorType::QUERY)); |
| EXPECT_EQ(query_match.image_url.spec(), "https://example.com/image.png"); |
| EXPECT_EQ(query_match.icon_url.spec(), "https://example.com/icon.png"); |
| EXPECT_EQ(query_match.keyword, u"keyword"); |
| EXPECT_TRUE(PageTransitionCoreTypeIs(query_match.transition, |
| ui::PAGE_TRANSITION_KEYWORD)); |
| EXPECT_EQ(query_match.from_keyword, true); |
| EXPECT_EQ(query_match.search_terms_args->search_terms, u"input title"); |
| |
| auto people_match = provider_->CreateMatch( |
| AutocompleteMatch::EnterpriseSearchAggregatorType::PEOPLE, true, {1000}, |
| "https://url.com/", "https://example.com/image.png", |
| "https://example.com/icon.png", u"Keyword People", u"input name", |
| u"keyword https://url.com/"); |
| EXPECT_EQ(people_match.relevance, 1000); |
| EXPECT_EQ(people_match.destination_url.spec(), "https://url.com/"); |
| EXPECT_EQ(people_match.fill_into_edit, u"keyword https://url.com/"); |
| EXPECT_EQ(people_match.enterprise_search_aggregator_type, |
| AutocompleteMatch::EnterpriseSearchAggregatorType::PEOPLE); |
| EXPECT_EQ(people_match.description, u"Keyword People"); |
| EXPECT_EQ(people_match.description_class, |
| primary_text_class( |
| AutocompleteMatch::EnterpriseSearchAggregatorType::PEOPLE)); |
| EXPECT_EQ(people_match.contents, u"input name"); |
| EXPECT_EQ(people_match.contents_class, |
| secondary_text_class( |
| AutocompleteMatch::EnterpriseSearchAggregatorType::PEOPLE)); |
| EXPECT_EQ(people_match.image_url.spec(), "https://example.com/image.png"); |
| EXPECT_EQ(people_match.icon_url.spec(), "https://example.com/icon.png"); |
| EXPECT_EQ(people_match.keyword, u"keyword"); |
| EXPECT_TRUE(PageTransitionCoreTypeIs(people_match.transition, |
| ui::PAGE_TRANSITION_KEYWORD)); |
| EXPECT_EQ(people_match.from_keyword, true); |
| EXPECT_EQ(people_match.search_terms_args, nullptr); |
| } |
| |
| // Test that the provider runs only when allowed. |
| TEST_F(EnterpriseSearchAggregatorProviderTest, IsProviderAllowed) { |
| AutocompleteInput input = CreateInput(u"text text", false); |
| |
| // Check `IsProviderAllowed()` returns true when all conditions pass. |
| EXPECT_TRUE(provider_->IsProviderAllowed(input)); |
| |
| { |
| // Should not be an incognito window. |
| EXPECT_CALL(*client_.get(), IsOffTheRecord()).WillRepeatedly(Return(true)); |
| EXPECT_FALSE(provider_->IsProviderAllowed(input)); |
| EXPECT_CALL(*client_.get(), IsOffTheRecord()).WillRepeatedly(Return(false)); |
| EXPECT_TRUE(provider_->IsProviderAllowed(input)); |
| } |
| |
| { |
| // Improve Search Suggest setting should be enabled. |
| EXPECT_CALL(*client_.get(), SearchSuggestEnabled()) |
| .WillRepeatedly(Return(false)); |
| EXPECT_FALSE(provider_->IsProviderAllowed(input)); |
| EXPECT_CALL(*client_.get(), SearchSuggestEnabled()) |
| .WillRepeatedly(Return(true)); |
| EXPECT_TRUE(provider_->IsProviderAllowed(input)); |
| } |
| |
| { |
| // Feature must be enabled. |
| scoped_config_.Get().enabled = false; |
| EXPECT_FALSE(provider_->IsProviderAllowed(input)); |
| scoped_config_.Get().enabled = true; |
| EXPECT_TRUE(provider_->IsProviderAllowed(input)); |
| } |
| |
| { |
| // The provider is run regardless of default search engine. |
| TemplateURLService* turl_service = client_->GetTemplateURLService(); |
| TemplateURLData data; |
| data.SetShortName(u"test"); |
| data.SetURL("https://www.yahoo.com/?q={searchTerms}"); |
| data.suggestions_url = "https://www.yahoo.com/complete/?q={searchTerms}"; |
| TemplateURL* new_default_provider = |
| turl_service->Add(std::make_unique<TemplateURL>(data)); |
| turl_service->SetUserSelectedDefaultSearchProvider(new_default_provider); |
| EXPECT_TRUE(provider_->IsProviderAllowed(input)); |
| TemplateURL* current_template_url = |
| const_cast<TemplateURL*>((provider_->template_url_).get()); |
| turl_service->SetUserSelectedDefaultSearchProvider(current_template_url); |
| turl_service->Remove(new_default_provider); |
| EXPECT_TRUE(provider_->IsProviderAllowed(input)); |
| } |
| |
| { |
| // Query must be at least 4 characters long in unscoped mode. |
| AutocompleteInput unscoped_input_long( |
| u"text", metrics::OmniboxEventProto::OTHER, TestSchemeClassifier()); |
| EXPECT_TRUE(provider_->IsProviderAllowed(unscoped_input_long)); |
| AutocompleteInput unscoped_input_short( |
| u"t", metrics::OmniboxEventProto::OTHER, TestSchemeClassifier()); |
| EXPECT_FALSE(provider_->IsProviderAllowed(unscoped_input_short)); |
| AutocompleteInput unscoped_empty_input( |
| u"", metrics::OmniboxEventProto::OTHER, TestSchemeClassifier()); |
| EXPECT_FALSE(provider_->IsProviderAllowed(unscoped_empty_input)); |
| } |
| |
| { |
| // Query must not be a url in unscoped mode |
| EXPECT_TRUE(provider_->IsProviderAllowed(input)); |
| AutocompleteInput query_input(u"https", metrics::OmniboxEventProto::OTHER, |
| TestSchemeClassifier()); |
| EXPECT_TRUE(provider_->IsProviderAllowed(query_input)); |
| AutocompleteInput person_query( |
| u"john doe", metrics::OmniboxEventProto::OTHER, TestSchemeClassifier()); |
| EXPECT_TRUE(provider_->IsProviderAllowed(person_query)); |
| AutocompleteInput url_input(u"www.web.site", |
| metrics::OmniboxEventProto::OTHER, |
| TestSchemeClassifier()); |
| EXPECT_FALSE(provider_->IsProviderAllowed(url_input)); |
| AutocompleteInput url_no_prefix( |
| u"john.com", metrics::OmniboxEventProto::OTHER, TestSchemeClassifier()); |
| EXPECT_FALSE(provider_->IsProviderAllowed(url_no_prefix)); |
| } |
| } |
| |
| // Test that a call to `Start()` will stop old requests to prevent their results |
| // from appearing with the new input. |
| TEST_F(EnterpriseSearchAggregatorProviderTest, StartCallsStop) { |
| scoped_config_.Get().enabled = false; |
| AutocompleteInput invalid_input = CreateInput(u"keyword text", false); |
| invalid_input.set_omit_asynchronous_matches(false); |
| |
| provider_->done_ = false; |
| provider_->Start(invalid_input, false); |
| EXPECT_TRUE(provider_->done()); |
| EXPECT_THAT(GetMatches(), testing::ElementsAre()); |
| } |
| |
| // Test that a call to `Start()` will clear cached matches and not send a new |
| // request if input (scoped) is empty. |
| TEST_F(EnterpriseSearchAggregatorProviderTest, |
| StartCallsStopForScopedEmptyInput) { |
| provider_->adjusted_input_ = CreateInput(u"keyword", true); |
| provider_->matches_ = {CreateAutocompleteMatch(u"https://cached.org/")}; |
| |
| provider_->Start(provider_->adjusted_input_, false); |
| EXPECT_TRUE(provider_->done()); |
| EXPECT_THAT(GetMatches(), testing::ElementsAre()); |
| } |
| |
| // Test that a call to `Start()` will not send a new request if input is zero |
| // suggest. This test also checks that both code paths, when `multiple_requests` |
| // equals true or false, work. |
| TEST_F(EnterpriseSearchAggregatorProviderTest, StartCallsStopForZeroSuggest) { |
| AutocompleteInput input = CreateInput(u"", false); |
| input.set_focus_type(metrics::INTERACTION_FOCUS); |
| provider_->adjusted_input_ = input; |
| provider_->matches_ = {CreateAutocompleteMatch(u"https://cached.org/")}; |
| |
| provider_->Start(input, false); |
| EXPECT_TRUE(provider_->done()); |
| EXPECT_THAT(GetMatches(), testing::ElementsAre()); |
| } |
| |
| // Test that a call to `Start()` will not set `done_` if |
| // `omit_asynchronous_matches` is true. |
| TEST_F(EnterpriseSearchAggregatorProviderTest, |
| StartLeavesDoneForOmitAsynchronousMatches) { |
| AutocompleteInput input = CreateInput(u"keyword text", false); |
| input.set_omit_asynchronous_matches(true); |
| |
| provider_->done_ = true; |
| provider_->Start(input, false); |
| EXPECT_TRUE(provider_->done()); |
| } |
| |
| // Test response is parsed accurately. |
| TEST_F(EnterpriseSearchAggregatorProviderTest, Parse) { |
| base::test::ScopedRestoreDefaultTimezone la_time("America/Los_Angeles"); |
| |
| scoped_config_.Get().relevance_scoring_mode = "server"; |
| |
| provider_->adjusted_input_ = CreateInput(u"john d", true); |
| StartAndComplete3Requests(200, kGoodJsonResponse); |
| |
| ACMatches matches = provider_->matches_; |
| ASSERT_EQ(matches.size(), 3u); |
| |
| EXPECT_EQ(matches[0].type, AutocompleteMatchType::NAVSUGGEST); |
| EXPECT_EQ(matches[0].relevance, 810); |
| EXPECT_EQ(matches[0].contents, |
| l10n_util::GetStringFUTF16(IDS_PERSON_SUGGESTION_DESCRIPTION, |
| u"keyword")); |
| EXPECT_EQ(matches[0].description, u"John Doe"); |
| EXPECT_EQ(matches[0].destination_url, |
| GURL("https://example.com/people/jdoe")); |
| EXPECT_EQ(matches[0].image_url, GURL("https://example.com/image.png")); |
| EXPECT_EQ(matches[0].icon_url, GURL("https://www.google.com/favicon.ico")); |
| EXPECT_TRUE(PageTransitionCoreTypeIs(matches[0].transition, |
| ui::PAGE_TRANSITION_KEYWORD)); |
| EXPECT_EQ(matches[0].fill_into_edit, |
| u"keyword https://example.com/people/jdoe"); |
| |
| EXPECT_EQ(matches[1].type, AutocompleteMatchType::NAVSUGGEST); |
| EXPECT_EQ(matches[1].relevance, 410); |
| EXPECT_EQ(matches[1].contents, u"10/15/07 - John Doe - Google Docs"); |
| EXPECT_EQ(matches[1].description, u"John's doodle"); |
| EXPECT_EQ(matches[1].destination_url, GURL("https://www.example.com")); |
| EXPECT_EQ(matches[1].image_url, GURL()); |
| EXPECT_EQ(matches[1].icon_url, GURL("https://example.com/icon.png")); |
| EXPECT_TRUE(PageTransitionCoreTypeIs(matches[1].transition, |
| ui::PAGE_TRANSITION_KEYWORD)); |
| EXPECT_EQ(matches[1].fill_into_edit, u"keyword https://www.example.com"); |
| |
| EXPECT_EQ(matches[2].type, AutocompleteMatchType::SEARCH_SUGGEST); |
| EXPECT_EQ(matches[2].relevance, 0); |
| EXPECT_EQ(matches[2].contents, u"John's Document 1"); |
| EXPECT_EQ(matches[2].description, u""); |
| EXPECT_EQ(matches[2].destination_url, |
| GURL("https://www.google.com/?q=John%27s+Document+1")); |
| EXPECT_EQ(matches[2].image_url, GURL()); |
| EXPECT_EQ(matches[2].icon_url, GURL()); |
| EXPECT_TRUE(PageTransitionCoreTypeIs(matches[2].transition, |
| ui::PAGE_TRANSITION_KEYWORD)); |
| EXPECT_EQ(matches[2].fill_into_edit, u"keyword John's Document 1"); |
| } |
| |
| // Test response is parsed accurately. |
| TEST_F(EnterpriseSearchAggregatorProviderTest, Parse_MultipleResponses) { |
| base::test::ScopedRestoreDefaultTimezone la_time("America/Los_Angeles"); |
| |
| scoped_config_.Get().relevance_scoring_mode = "server"; |
| |
| provider_->adjusted_input_ = CreateInput(u"john d", true); |
| StartAndComplete3Requests(200, kGoodJsonResponse); |
| |
| ACMatches matches = provider_->matches_; |
| ASSERT_EQ(matches.size(), 3u); |
| |
| EXPECT_EQ(matches[0].type, AutocompleteMatchType::NAVSUGGEST); |
| EXPECT_EQ(matches[0].relevance, 810); |
| EXPECT_EQ(matches[0].contents, |
| l10n_util::GetStringFUTF16(IDS_PERSON_SUGGESTION_DESCRIPTION, |
| u"keyword")); |
| EXPECT_EQ(matches[0].description, u"John Doe"); |
| EXPECT_EQ(matches[0].destination_url, |
| GURL("https://example.com/people/jdoe")); |
| EXPECT_EQ(matches[0].image_url, GURL("https://example.com/image.png")); |
| EXPECT_EQ(matches[0].icon_url, GURL("https://www.google.com/favicon.ico")); |
| EXPECT_TRUE(PageTransitionCoreTypeIs(matches[0].transition, |
| ui::PAGE_TRANSITION_KEYWORD)); |
| EXPECT_EQ(matches[0].fill_into_edit, |
| u"keyword https://example.com/people/jdoe"); |
| |
| EXPECT_EQ(matches[1].type, AutocompleteMatchType::NAVSUGGEST); |
| EXPECT_EQ(matches[1].relevance, 410); |
| EXPECT_EQ(matches[1].contents, u"10/15/07 - John Doe - Google Docs"); |
| EXPECT_EQ(matches[1].description, u"John's doodle"); |
| EXPECT_EQ(matches[1].destination_url, GURL("https://www.example.com")); |
| EXPECT_EQ(matches[1].image_url, GURL()); |
| EXPECT_EQ(matches[1].icon_url, GURL("https://example.com/icon.png")); |
| EXPECT_TRUE(PageTransitionCoreTypeIs(matches[1].transition, |
| ui::PAGE_TRANSITION_KEYWORD)); |
| EXPECT_EQ(matches[1].fill_into_edit, u"keyword https://www.example.com"); |
| |
| EXPECT_EQ(matches[2].type, AutocompleteMatchType::SEARCH_SUGGEST); |
| EXPECT_EQ(matches[2].relevance, 0); |
| EXPECT_EQ(matches[2].contents, u"John's Document 1"); |
| EXPECT_EQ(matches[2].description, u""); |
| EXPECT_EQ(matches[2].destination_url, |
| GURL("https://www.google.com/?q=John%27s+Document+1")); |
| EXPECT_EQ(matches[2].image_url, GURL()); |
| EXPECT_EQ(matches[2].icon_url, GURL()); |
| EXPECT_TRUE(PageTransitionCoreTypeIs(matches[2].transition, |
| ui::PAGE_TRANSITION_KEYWORD)); |
| EXPECT_EQ(matches[2].fill_into_edit, u"keyword John's Document 1"); |
| } |
| |
| // Test response is parsed accurately and image_url is adjusted appropriately. |
| TEST_F(EnterpriseSearchAggregatorProviderTest, ParseAndModifyImageUrls) { |
| provider_->adjusted_input_ = CreateInput(u"john d", true); |
| StartAndComplete3Requests(200, kGoodJsonResponseImageUrls); |
| |
| ACMatches matches = provider_->matches_; |
| ASSERT_EQ(matches.size(), 4u); |
| |
| EXPECT_EQ(matches[0].contents, |
| l10n_util::GetStringFUTF16(IDS_PERSON_SUGGESTION_DESCRIPTION, |
| u"keyword")); |
| EXPECT_EQ(matches[0].description, u"John Doe"); |
| EXPECT_EQ(matches[0].image_url, |
| GURL("https://lh3.googleusercontent.com/some/path-s100=s64")); |
| |
| EXPECT_EQ(matches[1].contents, |
| l10n_util::GetStringFUTF16(IDS_PERSON_SUGGESTION_DESCRIPTION, |
| u"keyword")); |
| EXPECT_EQ(matches[1].description, u"John Doe2"); |
| EXPECT_EQ(matches[1].image_url, |
| GURL("https://lh3.googleusercontent.com/some/path=s100")); |
| |
| EXPECT_EQ(matches[2].contents, |
| l10n_util::GetStringFUTF16(IDS_PERSON_SUGGESTION_DESCRIPTION, |
| u"keyword")); |
| EXPECT_EQ(matches[2].description, u"John Doe3"); |
| EXPECT_EQ(matches[2].image_url, |
| GURL("https://lh3.googleusercontent.com/some/path=abc-s64")); |
| |
| EXPECT_EQ(matches[3].contents, |
| l10n_util::GetStringFUTF16(IDS_PERSON_SUGGESTION_DESCRIPTION, |
| u"keyword")); |
| EXPECT_EQ(matches[3].description, u"John Doe4"); |
| EXPECT_EQ(matches[3].image_url, |
| GURL("https://lh3.googleusercontent.com/some/path=w100-h200")); |
| } |
| |
| // Test results with missing expected fields are skipped. |
| TEST_F(EnterpriseSearchAggregatorProviderTest, ParseWithMissingFields) { |
| provider_->adjusted_input_ = CreateInput(u"john d", true); |
| StartAndComplete3Requests(200, kMissingFieldsJsonResponse); |
| EXPECT_THAT( |
| GetMatches(), |
| testing::ElementsAre(u"https://example.com/people/jdoe", |
| u"https://www.example.com/", |
| u"https://www.google.com/?q=John%27s+Document+1")); |
| } |
| |
| // Test non-dict results are skipped. |
| TEST_F(EnterpriseSearchAggregatorProviderTest, ParseWithNonDict) { |
| AutocompleteInput input = CreateInput(u"john d", true); |
| provider_->adjusted_input_ = input; |
| |
| StartAndComplete3Requests(200, kNonDictJsonResponse); |
| EXPECT_THAT(GetMatches(), testing::ElementsAre()); |
| } |
| |
| // Test things work when using an unfeatured keyword. |
| TEST_F(EnterpriseSearchAggregatorProviderTest, UnfeaturedKeyword) { |
| // Unfeatured keyword must match a featured keyword according to current |
| // design. |
| TemplateURLData turl_data; |
| turl_data.SetShortName(u"unfeatured"); |
| turl_data.SetKeyword(u"unfeatured"); |
| turl_data.SetURL("http://www.yahoo.com/{searchTerms}"); |
| turl_data.is_active = TemplateURLData::ActiveStatus::kTrue; |
| turl_data.featured_by_policy = false; |
| turl_data.policy_origin = TemplateURLData::PolicyOrigin::kSearchAggregator; |
| client_->GetTemplateURLService()->Add( |
| std::make_unique<TemplateURL>(turl_data)); |
| AutocompleteInput input = CreateInput(u"unfeatured john d", true); |
| |
| EXPECT_CALL(*mock_listener_.get(), OnProviderUpdate(_, provider_.get())) |
| .Times(1); |
| provider_->Start(input, false); |
| StartAndComplete3Requests(200, kGoodJsonResponse); |
| EXPECT_EQ(provider_->matches_[0].keyword, u"unfeatured"); |
| EXPECT_THAT(GetMatches(), testing::ElementsAre( |
| u"https://example.com/people/jdoe", |
| u"https://www.example.com/", |
| u"http://www.yahoo.com/John's%20Document%201")); |
| } |
| |
| // Test things work in unscoped mode and query suggestions are skipped. |
| TEST_F(EnterpriseSearchAggregatorProviderTest, UnscopedMode) { |
| AutocompleteInput input = CreateInput(u"john d", false); |
| |
| EXPECT_CALL(*mock_listener_.get(), OnProviderUpdate(_, provider_.get())) |
| .Times(1); |
| provider_->Start(input, false); |
| StartAndCompleteRequests(200, {{}, kGoodJsonResponse, kGoodJsonResponse}); |
| EXPECT_THAT(GetMatches(), |
| testing::ElementsAre(u"https://example.com/people/jdoe", |
| u"https://www.example.com/")); |
| } |
| |
| TEST_F(EnterpriseSearchAggregatorProviderTest, Limits) { |
| // At most 2 per type when unscoped. Filtered matches shouldn't count against |
| // the limit. |
| provider_->adjusted_input_ = CreateInput(u"mango m", false); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse( |
| { |
| CreateQueryResult("grape-1-query"), |
| CreateQueryResult("grape-2-query"), |
| CreateQueryResult("grape-3-query"), |
| CreateQueryResult("mango-1-query"), |
| CreateQueryResult("mango-2-query"), |
| CreateQueryResult("mango-3-query"), |
| }, |
| { |
| CreatePeopleResult("displayName", "grape-1-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "grape-2-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "grape-3-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-1-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-2-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-3-people", "givenName", |
| "familyName"), |
| }, |
| { |
| CreateContentResult("grape-1-content", "https://url-grape-1/"), |
| CreateContentResult("grape-2-content", "https://url-grape-2/"), |
| CreateContentResult("grape-3-content", "https://url-grape-3/"), |
| CreateContentResult("mango-1-content", "https://url-mango-1/"), |
| CreateContentResult("mango-2-content", "https://url-mango-2/"), |
| CreateContentResult("mango-3-content", "https://url-mango-3/"), |
| })); |
| EXPECT_THAT( |
| GetScoredMatches(), |
| testing::ElementsAre( |
| ScoredMatch{u"https://example.com/people/mango-1-people", 607}, |
| ScoredMatch{u"https://example.com/people/mango-2-people", 606}, |
| ScoredMatch{u"https://url-mango-1/", 517}, |
| ScoredMatch{u"https://url-mango-2/", 516}, |
| ScoredMatch{u"https://www.google.com/?q=mango-1-query", 507}, |
| ScoredMatch{u"https://www.google.com/?q=mango-2-query", 506})); |
| |
| // At most 4 per type when scoped. Filtered matches shouldn't count against |
| // the limit. |
| provider_->adjusted_input_ = CreateInput(u"mango m", true); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse( |
| { |
| CreateQueryResult("grape-1-query"), |
| CreateQueryResult("grape-2-query"), |
| CreateQueryResult("grape-3-query"), |
| CreateQueryResult("mango-1-query"), |
| CreateQueryResult("mango-2-query"), |
| CreateQueryResult("mango-3-query"), |
| CreateQueryResult("mango-4-query"), |
| CreateQueryResult("mango-5-query"), |
| }, |
| { |
| CreatePeopleResult("displayName", "grape-1-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "grape-2-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "grape-3-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-1-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-2-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-3-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-4-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-5-people", "givenName", |
| "familyName"), |
| }, |
| { |
| CreateContentResult("grape-1-content", "https://url-grape-1/"), |
| CreateContentResult("grape-2-content", "https://url-grape-2/"), |
| CreateContentResult("grape-3-content", "https://url-grape-3/"), |
| CreateContentResult("mango-1-content", "https://url-mango-1/"), |
| CreateContentResult("mango-2-content", "https://url-mango-2/"), |
| CreateContentResult("mango-3-content", "https://url-mango-3/"), |
| CreateContentResult("mango-4-content", "https://url-mango-4/"), |
| CreateContentResult("mango-5-content", "https://url-mango-5/"), |
| })); |
| EXPECT_THAT( |
| GetScoredMatches(), |
| testing::ElementsAre( |
| ScoredMatch{u"https://example.com/people/mango-1-people", 607}, |
| ScoredMatch{u"https://example.com/people/mango-2-people", 606}, |
| ScoredMatch{u"https://example.com/people/mango-3-people", 605}, |
| ScoredMatch{u"https://example.com/people/mango-4-people", 604}, |
| ScoredMatch{u"https://url-mango-1/", 517}, |
| ScoredMatch{u"https://url-mango-2/", 516}, |
| ScoredMatch{u"https://url-mango-3/", 515}, |
| ScoredMatch{u"https://url-mango-4/", 514}, |
| ScoredMatch{u"https://www.google.com/?q=mango-1-query", 507}, |
| ScoredMatch{u"https://www.google.com/?q=mango-2-query", 506}, |
| ScoredMatch{u"https://www.google.com/?q=mango-3-query", 505}, |
| ScoredMatch{u"https://www.google.com/?q=mango-4-query", 504})); |
| |
| // Types that have less than 2 results aren't backfilled by other types. |
| provider_->adjusted_input_ = CreateInput(u"mango m", false); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse( |
| {}, {}, |
| { |
| CreateContentResult("grape-1-content", "https://url-grape-1/"), |
| CreateContentResult("grape-2-content", "https://url-grape-2/"), |
| CreateContentResult("grape-3-content", "https://url-grape-3/"), |
| CreateContentResult("mango-1-content", "https://url-mango-1/"), |
| CreateContentResult("mango-2-content", "https://url-mango-2/"), |
| CreateContentResult("mango-3-content", "https://url-mango-3/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url-mango-1/", 517}, |
| ScoredMatch{u"https://url-mango-2/", 516})); |
| |
| // The best 2 suggestions should be shown, even if they're not |
| // the 1st 2. |
| provider_->adjusted_input_ = CreateInput(u"mango mango-2 mango-3", false); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse( |
| {}, {}, |
| { |
| CreateContentResult("mango-1-content", "https://url-mango-1/"), |
| CreateContentResult("mango-2-content", "https://url-mango-2/"), |
| CreateContentResult("mango-3-content", "https://url-mango-3/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url-mango-2/", 519}, |
| ScoredMatch{u"https://url-mango-3/", 518})); |
| |
| // Can show more than 2 per type when scoped. |
| provider_->adjusted_input_ = CreateInput(u"mango m", true); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse( |
| { |
| CreateQueryResult("grape-1-query"), |
| CreateQueryResult("grape-2-query"), |
| CreateQueryResult("grape-3-query"), |
| CreateQueryResult("mango-1-query"), |
| CreateQueryResult("mango-2-query"), |
| CreateQueryResult("mango-3-query"), |
| }, |
| { |
| CreatePeopleResult("displayName", "grape-1-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "grape-2-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "grape-3-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-1-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-2-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-3-people", "givenName", |
| "familyName"), |
| }, |
| { |
| CreateContentResult("grape-1-content", "https://url-grape-1/"), |
| CreateContentResult("grape-2-content", "https://url-grape-2/"), |
| CreateContentResult("grape-3-content", "https://url-grape-3/"), |
| CreateContentResult("mango-1-content", "https://url-mango-1/"), |
| CreateContentResult("mango-2-content", "https://url-mango-2/"), |
| CreateContentResult("mango-3-content", "https://url-mango-3/"), |
| })); |
| EXPECT_THAT( |
| GetScoredMatches(), |
| testing::ElementsAre( |
| ScoredMatch{u"https://example.com/people/mango-1-people", 607}, |
| ScoredMatch{u"https://example.com/people/mango-2-people", 606}, |
| ScoredMatch{u"https://example.com/people/mango-3-people", 605}, |
| ScoredMatch{u"https://url-mango-1/", 517}, |
| ScoredMatch{u"https://url-mango-2/", 516}, |
| ScoredMatch{u"https://url-mango-3/", 515}, |
| ScoredMatch{u"https://www.google.com/?q=mango-1-query", 507}, |
| ScoredMatch{u"https://www.google.com/?q=mango-2-query", 506}, |
| ScoredMatch{u"https://www.google.com/?q=mango-3-query", 505})); |
| |
| // Limit low-quality suggestions. Only the 1st 2 matches are allowed to score |
| // lower than 500. Even if the 1st 2 matches score higher than 500, the |
| // remaining matches must also score higher than 500. |
| provider_->adjusted_input_ = CreateInput(u"m ma", false); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse( |
| { |
| CreateQueryResult("grape-1-query"), |
| CreateQueryResult("grape-2-query"), |
| CreateQueryResult("grape-3-query"), |
| CreateQueryResult("mango-1-query"), |
| CreateQueryResult("mango-2-query"), |
| CreateQueryResult("mango-3-query"), |
| }, |
| { |
| CreatePeopleResult("displayName", "grape-1-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "grape-2-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "grape-3-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-1-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-2-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-3-people", "givenName", |
| "familyName"), |
| }, |
| { |
| CreateContentResult("grape-1-content", "https://url-grape-1/"), |
| CreateContentResult("grape-2-content", "https://url-grape-2/"), |
| CreateContentResult("grape-3-content", "https://url-grape-3/"), |
| CreateContentResult("mango-1-content", "https://url-mango-1/"), |
| CreateContentResult("mango-2-content", "https://url-mango-2/"), |
| CreateContentResult("mango-3-content", "https://url-mango-3/"), |
| })); |
| EXPECT_THAT( |
| GetScoredMatches(), |
| testing::ElementsAre( |
| ScoredMatch{u"https://example.com/people/mango-1-people", 307}, |
| ScoredMatch{u"https://example.com/people/mango-2-people", 306})); |
| |
| // Scoped inputs have a higher limit of 8 matches allowed to score lower than |
| // 500. Even if the 1st 2 matches score higher than 500, the remaining matches |
| // must also score higher than 500. |
| provider_->adjusted_input_ = CreateInput(u"m ma", true); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse( |
| { |
| CreateQueryResult("grape-1-query"), |
| CreateQueryResult("grape-2-query"), |
| CreateQueryResult("grape-3-query"), |
| CreateQueryResult("mango-1-query"), |
| CreateQueryResult("mango-2-query"), |
| CreateQueryResult("mango-3-query"), |
| }, |
| { |
| CreatePeopleResult("displayName", "grape-1-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "grape-2-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "grape-3-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-1-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-2-people", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "mango-3-people", "givenName", |
| "familyName"), |
| }, |
| { |
| CreateContentResult("grape-1-content", "https://url-grape-1/"), |
| CreateContentResult("grape-2-content", "https://url-grape-2/"), |
| CreateContentResult("grape-3-content", "https://url-grape-3/"), |
| CreateContentResult("mango-1-content", "https://url-mango-1/"), |
| CreateContentResult("mango-2-content", "https://url-mango-2/"), |
| CreateContentResult("mango-3-content", "https://url-mango-3/"), |
| })); |
| EXPECT_THAT( |
| GetScoredMatches(), |
| testing::ElementsAre( |
| ScoredMatch{u"https://example.com/people/mango-1-people", 307}, |
| ScoredMatch{u"https://example.com/people/mango-2-people", 306}, |
| ScoredMatch{u"https://example.com/people/mango-3-people", 305}, |
| ScoredMatch{u"https://url-mango-1/", 217}, |
| ScoredMatch{u"https://url-mango-2/", 216}, |
| ScoredMatch{u"https://url-mango-3/", 215}, |
| ScoredMatch{u"https://www.google.com/?q=mango-1-query", 207}, |
| ScoredMatch{u"https://www.google.com/?q=mango-2-query", 206})); |
| } |
| |
| TEST_F(EnterpriseSearchAggregatorProviderTest, ServerRelevanceScoring) { |
| scoped_config_.Get().relevance_scoring_mode = "server"; |
| |
| provider_->adjusted_input_ = CreateInput(u"match m", true); |
| StartAndComplete3Requests( |
| 200, CreateResponse( |
| { |
| CreateQueryResult("matchQuery", 1.0), |
| // Results that don't match the input should still be scored |
| // and returned. |
| CreateQueryResult("query", 1.0), |
| CreateQueryResult("query2", 1.0), |
| CreateQueryResult("query3", 0.8), |
| }, |
| { |
| CreatePeopleResult("displayName", "matchUserName", |
| "givenName", "familyName", 0.7), |
| // A result with a score of 0 should not be returned. |
| CreatePeopleResult("displayName", "userName", "givenName", |
| "familyName", 0.0), |
| }, |
| { |
| CreateContentResult("matchTitle", "https://url/", 0.7), |
| CreateContentResult("title", "https://url2/", 0.7), |
| CreateContentResult("title2", "https://url3/", 0.3), |
| })); |
| EXPECT_THAT( |
| GetScoredMatches(), |
| testing::ElementsAre( |
| ScoredMatch{u"https://www.google.com/?q=matchQuery", 1010}, |
| ScoredMatch{u"https://www.google.com/?q=query", 1009}, |
| ScoredMatch{u"https://www.google.com/?q=query2", 1008}, |
| ScoredMatch{u"https://www.google.com/?q=query3", 807}, |
| ScoredMatch{u"https://example.com/people/matchUserName", 710}, |
| ScoredMatch{u"https://url/", 710}, ScoredMatch{u"https://url2/", 709}, |
| ScoredMatch{u"https://url3/", 308})); |
| } |
| |
| TEST_F(EnterpriseSearchAggregatorProviderTest, MixedRelevanceScoring) { |
| scoped_config_.Get().relevance_scoring_mode = "mixed"; |
| |
| std::string response = CreateResponse( |
| { |
| CreateQueryResult("matchQuery", 1.0), |
| CreateQueryResult("query", 1.0), |
| }, |
| { |
| CreatePeopleResult("displayName", "matchUserName", "givenName", |
| "familyName", 0.7), |
| CreatePeopleResult("displayName", "userName", "givenName", |
| "familyName", 0.6), |
| }, |
| { |
| CreateContentResult("matchTitle", "https://url/", 0.7), |
| CreateContentResult("title2", "https://url2/", 0.3), |
| }); |
| |
| // Scoped mode should use server-provided relevance scores. |
| provider_->adjusted_input_ = CreateInput(u"match m", true); |
| StartAndComplete3Requests(200, response); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre( |
| ScoredMatch{u"https://www.google.com/?q=matchQuery", 1010}, |
| ScoredMatch{u"https://www.google.com/?q=query", 1009}, |
| ScoredMatch{u"https://example.com/people/matchUserName", 710}, |
| ScoredMatch{u"https://url/", 710}, |
| ScoredMatch{u"https://example.com/people/userName", 609}, |
| ScoredMatch{u"https://url2/", 309})); |
| |
| // Unscoped mode should use client-calculated relevance scores. |
| provider_->adjusted_input_ = CreateInput(u"match m", false); |
| StartAndComplete3Requests(200, response); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre( |
| ScoredMatch{u"https://example.com/people/matchUserName", 610}, |
| ScoredMatch{u"https://url/", 520}, |
| ScoredMatch{u"https://www.google.com/?q=matchQuery", 510})); |
| } |
| |
| TEST_F(EnterpriseSearchAggregatorProviderTest, LocalRelevanceScoring) { |
| // Results that don't match the input should be filtered out. |
| provider_->adjusted_input_ = CreateInput(u"match m", true); |
| StartAndComplete3Requests( |
| 200, CreateResponse( |
| { |
| CreateQueryResult("query"), |
| CreateQueryResult("matchQuery"), |
| }, |
| { |
| CreatePeopleResult("displayName", "userName", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "matchUserName", |
| "givenName", "familyName"), |
| }, |
| { |
| CreateContentResult("title", "https://url/"), |
| CreateContentResult("matchTitle", "https://url/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre( |
| ScoredMatch{u"https://example.com/people/matchUserName", 609}, |
| ScoredMatch{u"https://url/", 519}, |
| ScoredMatch{u"https://www.google.com/?q=matchQuery", 509}, |
| FieldsAre(_, 0), FieldsAre(_, 0), FieldsAre(_, 0))); |
| |
| // Score using weighted sum of matches. |
| provider_->adjusted_input_ = CreateInput(u"zero on tw th", true); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse( |
| {}, {}, |
| { |
| CreateContentResult("zero", "https://url-0/"), |
| CreateContentResult("zero one", "https://url-01/"), |
| CreateContentResult("zero one two", "https://url-012/"), |
| CreateContentResult("zero one two three", "https://url-0123/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url-0123/", 717}, |
| ScoredMatch{u"https://url-012/", 618}, |
| ScoredMatch{u"https://url-01/", 519}, |
| FieldsAre(_, 0))); |
| |
| // Duplicate matches do not count. |
| // - If the input repeats a word, only 1 should count. |
| // - If the result field repeats a word, only 1 should count. |
| // - If a word appears in multiple result fields, only 1 should count. |
| provider_->adjusted_input_ = CreateInput(u"one one", true); |
| StartAndComplete3Requests( |
| 200, CreateResponse({}, {}, |
| { |
| CreateContentResultWithOwnerEmail( |
| "one one", "one one", "https://url-1/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url-1/", 420})); |
| |
| // Each input word can match only 1 result word. |
| provider_->adjusted_input_ = CreateInput(u"one one", true); |
| StartAndComplete3Requests( |
| 200, CreateResponse({}, {}, |
| { |
| CreateContentResult("one oneTwo", "https://url/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url/", 420})); |
| |
| // A result word can match multiple input words. This is just a side effect |
| // of the implementation rather than intentional design. |
| provider_->adjusted_input_ = CreateInput(u"one on o", true); |
| StartAndComplete3Requests( |
| 200, CreateResponse({}, {}, |
| { |
| CreateContentResult("one", "https://url/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url/", 620})); |
| |
| // Matches outside contents and description contribute less to the score. |
| provider_->adjusted_input_ = CreateInput(u"one two three four five", true); |
| StartAndComplete3Requests( |
| 200, CreateResponse( |
| {}, {}, |
| { |
| CreateContentResultWithOwnerEmail( |
| "title one", "two three four five", "https://inside/"), |
| CreateContentResultWithOwnerEmail( |
| "title", "one two three four five", "https://outside/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://inside/", 820}, |
| ScoredMatch{u"https://outside/", 519})); |
| |
| // Short input words contribute less to the score. |
| provider_->adjusted_input_ = CreateInput(u"on two three four five", true); |
| StartAndComplete3Requests( |
| 200, CreateResponse({}, {}, |
| { |
| CreateContentResultWithOwnerEmail( |
| "one", "two three four five", "https://url/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url/", 520})); |
| |
| // Short input words contribute less to score, except for exact (non-prefix) |
| // matches in people suggestions. |
| provider_->adjusted_input_ = CreateInput(u"weak ab", true); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse( |
| {}, |
| { |
| {CreatePeopleResultWithoutEmailField("ab", "ab", "weak", "")}, |
| {CreatePeopleResultWithoutEmailField("abc", "abc", "weak", "")}, |
| }, |
| { |
| CreateContentResultWithOwnerEmail("ab", "weak", |
| "https://url-ab/"), |
| CreateContentResultWithOwnerEmail("abc", "weak", |
| "https://url-abc/"), |
| })); |
| EXPECT_THAT( |
| GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://example.com/people/ab", 610}, |
| ScoredMatch{u"https://example.com/people/abc", 309}, |
| ScoredMatch{u"https://url-ab/", 220}, |
| ScoredMatch{u"https://url-abc/", 219})); |
| |
| // For all suggestions, long input words contribute fully to the score |
| // regardless of whether they fully or prefix match. |
| provider_->adjusted_input_ = CreateInput(u"weak abc", true); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse( |
| {}, |
| { |
| {CreatePeopleResultWithoutEmailField("abc", "abc", "weak", "")}, |
| {CreatePeopleResultWithoutEmailField("abcd", "abcd", "weak", "")}, |
| }, |
| { |
| CreateContentResultWithOwnerEmail("abc", "weak", |
| "https://url-abc/"), |
| CreateContentResultWithOwnerEmail("abcd", "weak", |
| "https://url-abcd/"), |
| })); |
| EXPECT_THAT( |
| GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://example.com/people/abc", 610}, |
| ScoredMatch{u"https://example.com/people/abcd", 609}, |
| ScoredMatch{u"https://url-abc/", 520}, |
| ScoredMatch{u"https://url-abcd/", 519})); |
| |
| // Matches outside human-readable fields (e.g. URL) aren't considered in |
| // scoring. |
| (provider_->adjusted_input_) = CreateInput(u"title url", true); |
| StartAndComplete3Requests( |
| 200, CreateResponse({}, {}, |
| { |
| CreateContentResult("title", "https://url1/"), |
| CreateContentResult("title", "https://url2/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url1/", 420}, |
| ScoredMatch{u"https://url2/", 419})); |
| |
| // Suggestions that match every input words, when there are at least 2, should |
| // be scored higher. |
| (provider_->adjusted_input_) = CreateInput(u"one two", true); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse({}, {}, |
| { |
| CreateContentResult("one two three", "https://url/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url/", 1020})); |
| |
| // Suggestions that match every input words, when there is not at least 2, |
| // should not be scored higher. |
| (provider_->adjusted_input_) = CreateInput(u"one", true); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse({}, {}, |
| { |
| CreateContentResult("one two three", "https://url/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url/", 420})); |
| |
| // Suggestions that match at least 2 but not all inputs words should not be |
| // scored higher. |
| (provider_->adjusted_input_) = CreateInput(u"one two four", true); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse({}, {}, |
| { |
| CreateContentResult("one two three", "https://url/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url/", 820})); |
| |
| // Require at least 1 strong match or 2 weak matches. |
| (provider_->adjusted_input_) = CreateInput(u"title", true); |
| StartAndComplete3Requests( |
| 200, CreateResponse({}, {}, |
| { |
| CreateContentResult("title", "https://url/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url/", 420})); |
| |
| // When unscoped, requires at least 1 strong match or 2 weak matches. |
| (provider_->adjusted_input_) = CreateInput(u"user gmail", false); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse({}, {}, |
| { |
| CreateContentResultWithOwnerEmail( |
| "title", "user@example.com", "https://url-1/"), |
| CreateContentResultWithOwnerEmail( |
| "title", "user@gmail.com", "https://url-2/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url-2/", 219}, |
| FieldsAre(_, 0))); |
| |
| // When scoped, does not require at least 1 strong match or 2 weak matches. |
| (provider_->adjusted_input_) = CreateInput(u"user gmail", true); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse({}, {}, |
| { |
| CreateContentResultWithOwnerEmail( |
| "title", "user@example.com", "https://url-1/"), |
| CreateContentResultWithOwnerEmail( |
| "title", "user@gmail.com", "https://url-2/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url-2/", 219}, |
| ScoredMatch{u"https://url-1/", 120})); |
| |
| // Require at least half the input words to match. |
| provider_->adjusted_input_ = CreateInput(u"title x y", true); |
| StartAndComplete3Requests( |
| 200, CreateResponse({}, {}, |
| { |
| CreateContentResult("title", "https://url/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), testing::ElementsAre(FieldsAre(_, 0))); |
| |
| // People matches should be boosted. Also test that people matches without |
| // email fields are still scored appropriately. |
| provider_->adjusted_input_ = CreateInput(u"input i", true); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse( |
| { |
| CreateQueryResult("input"), |
| }, |
| { |
| CreatePeopleResult("displayName input", "userName", "givenName", |
| "familyName"), |
| CreatePeopleResult("displayName", "NoMatchUserName", "givenName", |
| "familyName"), |
| CreatePeopleResultWithoutEmailField( |
| "displayName input", "userName2", "givenName", "familyName"), |
| CreatePeopleResultWithoutEmailField( |
| "displayName", "NoMatchUserName2", "givenName", "familyName"), |
| }, |
| { |
| CreateContentResult("title input", "https://url/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre( |
| ScoredMatch{u"https://example.com/people/userName", 610}, |
| ScoredMatch{u"https://example.com/people/userName2", 608}, |
| ScoredMatch{u"https://url/", 520}, |
| ScoredMatch{u"https://www.google.com/?q=input", 510}, |
| FieldsAre(_, 0), FieldsAre(_, 0))); |
| |
| // People matches with exact email matches should be boosted. |
| provider_->adjusted_input_ = CreateInput(u"userName d", true); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse( |
| { |
| CreateQueryResult("userName"), |
| }, |
| { |
| CreatePeopleResult("userName displayName", "userName", |
| "givenName", "familyName"), |
| CreatePeopleResult("userName displayName", "userNamePrefixMatch", |
| "givenName", "familyName"), |
| CreatePeopleResult("userName displayName", "NoMatchUserName", |
| "givenName", "familyName"), |
| }, |
| { |
| CreateContentResult("title userName", "https://url/"), |
| })); |
| EXPECT_THAT( |
| GetScoredMatches(), |
| testing::ElementsAre( |
| ScoredMatch{u"https://example.com/people/userName", 1010}, |
| ScoredMatch{u"https://example.com/people/userNamePrefixMatch", 609}, |
| ScoredMatch{u"https://example.com/people/NoMatchUserName", 608}, |
| ScoredMatch{u"https://url/", 420}, |
| ScoredMatch{u"https://www.google.com/?q=userName", 410})); |
| |
| // People matches must match all input words. |
| provider_->adjusted_input_ = CreateInput(u"query q unmatched", true); |
| StartAndComplete3Requests( |
| 200, CreateResponse( |
| { |
| CreateQueryResult("query"), |
| }, |
| { |
| CreatePeopleResult("displayName query", "userName", |
| "givenName", "familyName"), |
| CreatePeopleResult("displayName", "matchUserName", |
| "givenName", "familyName"), |
| }, |
| { |
| CreateContentResult("title query", "https://url/"), |
| })); |
| EXPECT_THAT( |
| GetScoredMatches(), |
| testing::ElementsAre(ScoredMatch{u"https://url/", 520}, |
| ScoredMatch{u"https://www.google.com/?q=query", 510}, |
| FieldsAre(_, 0), FieldsAre(_, 0))); |
| |
| // When content and query matches equally match the input, content matches |
| // should be preferred. |
| provider_->adjusted_input_ = CreateInput(u"query", true); |
| StartAndComplete3Requests( |
| 200, CreateResponse( |
| { |
| CreateQueryResult("query"), |
| }, |
| {}, |
| { |
| CreateContentResult("query", "https://url/"), |
| })); |
| EXPECT_THAT(GetScoredMatches(), |
| testing::ElementsAre( |
| ScoredMatch{u"https://url/", 420}, |
| ScoredMatch{u"https://www.google.com/?q=query", 410})); |
| } |
| |
| TEST_F(EnterpriseSearchAggregatorProviderTest, |
| ContentSuggestionTypeDescriptions) { |
| provider_->adjusted_input_ = CreateInput(u"input", true); |
| StartAndComplete3Requests( |
| 200, |
| CreateResponse( |
| {}, {}, |
| { |
| // Verifies use of MIME type. |
| CreateContentResultWithTypes( |
| "Evolution of Dance", "https://url1/", "video/quicktime", ""), |
| // Verifies use of source type. |
| CreateContentResultWithTypes("Uh oh", "https://url2/", "", |
| "buganizer"), |
| // Verifies that MIME type takes precedent over source type. |
| CreateContentResultWithTypes( |
| "Same thing we do every night, Pinky", "https://url3/", |
| "image/png", "salesforce"), |
| })); |
| ACMatches matches = provider_->matches_; |
| ASSERT_EQ(matches.size(), 3u); |
| |
| // Verifies use of MIME type. |
| EXPECT_EQ(matches[0].contents, u"QuickTime Video"); |
| EXPECT_EQ(matches[0].description, u"Evolution of Dance"); |
| EXPECT_EQ(matches[0].destination_url, GURL("https://url1/")); |
| |
| // Verifies use of source type. |
| EXPECT_EQ(matches[1].contents, u"Buganizer Issue"); |
| EXPECT_EQ(matches[1].description, u"Uh oh"); |
| EXPECT_EQ(matches[1].destination_url, GURL("https://url2/")); |
| |
| // Verifies that MIME type takes precedent over source type. |
| EXPECT_EQ(matches[2].contents, u"PNG Image"); |
| EXPECT_EQ(matches[2].description, u"Same thing we do every night, Pinky"); |
| EXPECT_EQ(matches[2].destination_url, GURL("https://url3/")); |
| } |
| |
| TEST_F(EnterpriseSearchAggregatorProviderSingleRequestTest, |
| Logging_SingleRequest) { |
| // The code flow is: |
| // 1) `Start()` |
| // 2) `Run()` is invoked from `Start()` after a potential debouncing. |
| // 3) A request is asyncly made to Vertex AI backend once auth token is ready. |
| // 4) A response is asyncly received from the Vertex AI backend. |
| // At any point, the chain of events can be interrupted by a `Stop()` |
| // invocation; usually when there's a new input. The below 3 cases test the |
| // logged histograms when `Stop()` is invoked after steps 2, 3, and after the |
| // request is completed. |
| |
| { |
| SCOPED_TRACE("Case: Stop() before Run()."); |
| ExpectedHistogram expected_histogram{}; |
| provider_->Stop(AutocompleteStopReason::kClobbered); |
| } |
| |
| { |
| SCOPED_TRACE("Case: Stop() before request started."); |
| ExpectedHistogram expected_histogram{}; |
| provider_->requests_[0].Reset(true); |
| provider_->Stop(AutocompleteStopReason::kClobbered); |
| } |
| |
| { |
| SCOPED_TRACE("Case: Stop() before response."); |
| ExpectedHistogram expected_histogram{.unsliced_interrupted = true}; |
| StartRequests({0}); |
| provider_->Stop(AutocompleteStopReason::kClobbered); |
| } |
| |
| { |
| SCOPED_TRACE("Case: Request complete"); |
| ExpectedHistogram expected_histogram{.unsliced_completed = true, |
| .unsliced_count = 3}; |
| StartAndCompleteRequests(200, {{kGoodJsonResponse}}); |
| } |
| } |
| |
| TEST_F(EnterpriseSearchAggregatorProviderTest, Logging_MultipleRequests) { |
| // The code flow is: |
| // 1) `Start()` |
| // 2) `Run()` is invoked from `Start()` after a potential debouncing. |
| // 3) A request is asyncly made to Vertex AI backend once auth token is ready. |
| // 4) A response is asyncly received from the Vertex AI backend. |
| // At any point, the chain of events can be interrupted by a `Stop()` |
| // invocation; usually when there's a new input. The below 3 cases test the |
| // logged histograms when `Stop()` is invoked after steps 2, 3, and after the |
| // request is completed. |
| |
| // Even though this test is testing when `multiple_requests` is enabled, the 3 |
| // cases below have 1 request starting and/or completing. |
| |
| { |
| SCOPED_TRACE("Case: Stop() before Run()."); |
| ExpectedHistogram expected_histogram{}; |
| provider_->Stop(AutocompleteStopReason::kClobbered); |
| } |
| |
| { |
| SCOPED_TRACE("Case: Stop() before request started."); |
| ExpectedHistogram expected_histogram{}; |
| provider_->requests_[0].Reset(false); |
| provider_->requests_[1].Reset(true); |
| provider_->Stop(AutocompleteStopReason::kClobbered); |
| } |
| |
| { |
| SCOPED_TRACE("Case: Stop() before response."); |
| ExpectedHistogram expected_histogram{.unsliced_interrupted = true, |
| .query_interrupted = true}; |
| StartRequests({0}); |
| provider_->Stop(AutocompleteStopReason::kClobbered); |
| } |
| |
| { |
| SCOPED_TRACE("Case: Request complete"); |
| ExpectedHistogram expected_histogram{ |
| .unsliced_completed = true, |
| .query_completed = true, |
| .people_completed = true, |
| .content_completed = true, |
| .unsliced_count = 3, |
| .query_count = 1, |
| .people_count = 1, |
| .content_count = 1, |
| }; |
| StartAndComplete3Requests(200, kGoodJsonResponse); |
| } |
| |
| { |
| SCOPED_TRACE("Case: Some requests complete"); |
| ExpectedHistogram expected_histogram{.content_completed = true, |
| .content_count = 1}; |
| StartRequests({1, 2}); // Start people & content requests. |
| CompleteRequest(2, 200, kGoodJsonResponse); // Complete content request. |
| EXPECT_FALSE(provider_->done_); |
| expected_histogram.Verify(); |
| |
| expected_histogram.unsliced_interrupted = true; |
| expected_histogram.people_interrupted = true; |
| provider_->Stop(AutocompleteStopReason::kClobbered); |
| } |
| } |
| |
| TEST_F(EnterpriseSearchAggregatorProviderTest, |
| Caching_Logging_MultipleRequests) { |
| { |
| SCOPED_TRACE("1st autocomplete pass to populate query and people caches"); |
| // Use a keyword input to avoid filtering low quality matches. |
| provider_->adjusted_input_ = CreateInput(u"input", true); |
| ExpectedHistogram expected_histogram{}; |
| |
| { |
| SCOPED_TRACE("Start all 3 requests"); |
| StartRequests({0, 1, 2}); |
| EXPECT_THAT(GetMatches(), testing::ElementsAre()); |
| EXPECT_FALSE(provider_->done_); |
| expected_histogram.Verify(); |
| } |
| |
| { |
| SCOPED_TRACE("Resolve people request"); |
| expected_histogram.people_completed = true; |
| expected_histogram.people_count = 1; |
| CompleteRequest( |
| 1, 200, |
| CreateResponse({}, |
| {CreatePeopleResult("Justin Timberlake", "JT", |
| "Justin", "Timberlake")}, |
| {})); |
| EXPECT_THAT(GetMatches(), |
| testing::ElementsAre(u"https://example.com/people/JT")); |
| EXPECT_FALSE(provider_->done_); |
| expected_histogram.Verify(); |
| } |
| |
| { |
| SCOPED_TRACE("Resolve query request"); |
| expected_histogram.query_completed = true; |
| expected_histogram.query_count = 1; |
| CompleteRequest( |
| 0, 200, |
| {{CreateResponse({CreateQueryResult("Prince of Pop")}, {}, {})}}); |
| EXPECT_THAT(GetMatches(), testing::ElementsAre( |
| u"https://www.google.com/?q=Prince+of+Pop", |
| u"https://example.com/people/JT")); |
| EXPECT_FALSE(provider_->done_); |
| expected_histogram.Verify(); |
| } |
| |
| { |
| SCOPED_TRACE("Resolve content request"); |
| expected_histogram.unsliced_completed = true; |
| expected_histogram.content_completed = true; |
| expected_histogram.unsliced_count = 3; |
| expected_histogram.content_count = 1; |
| CompleteRequest( |
| 2, 200, |
| CreateResponse({}, {}, |
| {CreateContentResult( |
| "My Love", "https://youtube.com/q=my+love")})); |
| EXPECT_THAT(GetMatches(), testing::ElementsAre( |
| u"https://www.google.com/?q=Prince+of+Pop", |
| u"https://example.com/people/JT", |
| u"https://youtube.com/q=my+love")); |
| EXPECT_TRUE(provider_->done_); |
| expected_histogram.Verify(); |
| } |
| } |
| |
| { |
| SCOPED_TRACE("2nd autocomplete pass to use and clear caches"); |
| ExpectedHistogram expected_histogram{}; |
| |
| { |
| SCOPED_TRACE("Start people and content requests"); |
| StartRequests({1, 2}); |
| // Should still show cached people and content matches. Should clear |
| // cached query matches since we know there won't be new query matches. |
| EXPECT_THAT(GetMatches(), |
| testing::ElementsAre(u"https://example.com/people/JT", |
| u"https://youtube.com/q=my+love")); |
| EXPECT_FALSE(provider_->done_); |
| expected_histogram.Verify(); |
| } |
| |
| { |
| SCOPED_TRACE("Resolve people request"); |
| expected_histogram.people_completed = true; |
| expected_histogram.people_count = 1; |
| CompleteRequest( |
| 1, 200, |
| CreateResponse( |
| {}, |
| {CreatePeopleResult("Chris Brown", "Breezy", "Chris", "Brown")}, |
| {})); |
| // Should not show stale people match since new people matches are |
| // available. Should still show cached content match. |
| EXPECT_THAT(GetMatches(), |
| testing::ElementsAre(u"https://example.com/people/Breezy", |
| u"https://youtube.com/q=my+love")); |
| EXPECT_FALSE(provider_->done_); |
| expected_histogram.Verify(); |
| } |
| |
| { |
| SCOPED_TRACE("Resolve content request"); |
| // Should log completed even though query request was not completed |
| // because it was not started to begin with. |
| expected_histogram.unsliced_completed = true; |
| expected_histogram.content_completed = true; |
| expected_histogram.unsliced_count = 2; |
| expected_histogram.content_count = 1; |
| CompleteRequest(2, 200, |
| CreateResponse({}, {}, |
| {CreateContentResult( |
| "Gimme That", |
| "https://youtube.com/q=gimme+that")})); |
| // Should show no stale matches. |
| EXPECT_THAT(GetMatches(), |
| testing::ElementsAre(u"https://example.com/people/Breezy", |
| u"https://youtube.com/q=gimme+that")); |
| EXPECT_TRUE(provider_->done_); |
| expected_histogram.Verify(); |
| } |
| } |
| |
| { |
| SCOPED_TRACE("3rd autocomplete pass, empty and bad responses clear cache"); |
| ExpectedHistogram expected_histogram{}; |
| |
| { |
| SCOPED_TRACE("Start people and content requests"); |
| StartRequests({1, 2}); |
| // Should still show cached people and content matches. |
| EXPECT_THAT(GetMatches(), |
| testing::ElementsAre(u"https://example.com/people/Breezy", |
| u"https://youtube.com/q=gimme+that")); |
| EXPECT_FALSE(provider_->done_); |
| expected_histogram.Verify(); |
| } |
| |
| { |
| SCOPED_TRACE("Resolve empty people request"); |
| expected_histogram.people_completed = true; |
| expected_histogram.people_count = 0; |
| CompleteRequest(1, 200, CreateResponse({}, {}, {})); |
| // Should clear people cache. |
| EXPECT_THAT(GetMatches(), |
| testing::ElementsAre(u"https://youtube.com/q=gimme+that")); |
| EXPECT_FALSE(provider_->done_); |
| expected_histogram.Verify(); |
| } |
| |
| { |
| SCOPED_TRACE("Resolve content request with 404"); |
| // Should log completed even though query request was not completed |
| // because it was not started to begin with. |
| expected_histogram.unsliced_completed = true; |
| expected_histogram.content_completed = true; |
| expected_histogram.unsliced_count = 0; |
| expected_histogram.content_count = 0; |
| CompleteRequest(2, 404, |
| CreateResponse({}, {}, |
| {CreateContentResult( |
| "Gimme That", |
| "https://youtube.com/q=gimme+that")})); |
| EXPECT_THAT(GetMatches(), testing::ElementsAre()); |
| EXPECT_TRUE(provider_->done_); |
| expected_histogram.Verify(); |
| } |
| } |
| } |