1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "components/spellcheck/browser/spelling_service_client.h"
6 
7 #include <stddef.h>
8 
9 #include <memory>
10 #include <string>
11 #include <vector>
12 
13 #include "base/bind.h"
14 #include "base/json/json_reader.h"
15 #include "base/stl_util.h"
16 #include "base/strings/stringprintf.h"
17 #include "base/strings/utf_string_conversions.h"
18 #include "base/test/bind_test_util.h"
19 #include "base/values.h"
20 #include "chrome/test/base/testing_profile.h"
21 #include "components/prefs/pref_service.h"
22 #include "components/spellcheck/browser/pref_names.h"
23 #include "components/spellcheck/common/spellcheck_result.h"
24 #include "content/public/test/browser_task_environment.h"
25 #include "net/base/load_flags.h"
26 #include "net/http/http_util.h"
27 #include "services/network/public/cpp/shared_url_loader_factory.h"
28 #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
29 #include "services/network/test/test_url_loader_factory.h"
30 #include "services/network/test/test_utils.h"
31 #include "testing/gtest/include/gtest/gtest.h"
32 
33 namespace {
34 
35 struct SpellingServiceTestCase {
36   const wchar_t* request_text;
37   std::string sanitized_request_text;
38   SpellingServiceClient::ServiceType request_type;
39   net::HttpStatusCode response_status;
40   std::string response_data;
41   bool success;
42   const char* corrected_text;
43   std::string language;
44 };
45 
46 // A class derived from the SpellingServiceClient class used by the
47 // SpellingServiceClientTest class. This class sets the URLLoaderFactory so
48 // tests can control requests and responses.
49 class TestingSpellingServiceClient : public SpellingServiceClient {
50  public:
TestingSpellingServiceClient()51   TestingSpellingServiceClient()
52       : success_(false),
53         test_shared_loader_factory_(
54             base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
55                 &test_url_loader_factory_)) {
56     SetURLLoaderFactoryForTesting(test_shared_loader_factory_);
57   }
~TestingSpellingServiceClient()58   ~TestingSpellingServiceClient() {}
59 
SetExpectedTextCheckResult(bool success,const std::string & sanitized_request_text,const char * text)60   void SetExpectedTextCheckResult(bool success,
61                                   const std::string& sanitized_request_text,
62                                   const char* text) {
63     success_ = success;
64     sanitized_request_text_ = sanitized_request_text;
65     corrected_text_.assign(base::UTF8ToUTF16(text));
66   }
67 
VerifyResponse(bool success,const base::string16 & request_text,const std::vector<SpellCheckResult> & results)68   void VerifyResponse(bool success,
69                       const base::string16& request_text,
70                       const std::vector<SpellCheckResult>& results) {
71     EXPECT_EQ(success_, success);
72     base::string16 text(base::UTF8ToUTF16(sanitized_request_text_));
73     for (auto it = results.begin(); it != results.end(); ++it) {
74       text.replace(it->location, it->length, it->replacements[0]);
75     }
76     EXPECT_EQ(corrected_text_, text);
77   }
78 
ParseResponseSuccess(const std::string & data)79   bool ParseResponseSuccess(const std::string& data) {
80     std::vector<SpellCheckResult> results;
81     return ParseResponse(data, &results);
82   }
83 
test_url_loader_factory()84   network::TestURLLoaderFactory* test_url_loader_factory() {
85     return &test_url_loader_factory_;
86   }
87 
88  private:
89   bool success_;
90   std::string sanitized_request_text_;
91   base::string16 corrected_text_;
92   network::TestURLLoaderFactory test_url_loader_factory_;
93   scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
94 };
95 
96 // A test class used for testing the SpellingServiceClient class. This class
97 // implements a callback function used by the SpellingServiceClient class to
98 // monitor the class calls the callback with expected results.
99 class SpellingServiceClientTest
100     : public testing::TestWithParam<SpellingServiceTestCase> {
101  public:
OnTextCheckComplete(int tag,bool success,const base::string16 & text,const std::vector<SpellCheckResult> & results)102   void OnTextCheckComplete(int tag,
103                            bool success,
104                            const base::string16& text,
105                            const std::vector<SpellCheckResult>& results) {
106     client_.VerifyResponse(success, text, results);
107   }
108 
109  protected:
GetExpectedCountry(const std::string & language,std::string * country)110   bool GetExpectedCountry(const std::string& language, std::string* country) {
111     static const struct {
112       const char* language;
113       const char* country;
114     } kCountries[] = {
115         {"af", "ZAF"}, {"en", "USA"},
116     };
117     for (size_t i = 0; i < base::size(kCountries); ++i) {
118       if (!language.compare(kCountries[i].language)) {
119         country->assign(kCountries[i].country);
120         return true;
121       }
122     }
123     return false;
124   }
125 
126   content::BrowserTaskEnvironment task_environment_;
127   TestingSpellingServiceClient client_;
128   TestingProfile profile_;
129 };
130 
131 }  // namespace
132 
133 // Verifies that SpellingServiceClient::RequestTextCheck() creates a JSON
134 // request sent to the Spelling service as we expect. This test also verifies
135 // that it parses a JSON response from the service and calls the callback
136 // function. To avoid sending real requests to the service, this test uses a
137 // subclass of SpellingServiceClient that in turn sets the client's URL loader
138 // factory to a TestURLLoaderFactory. The client thinks it is issuing real
139 // network requests, but in fact the responses are entirely under our control
140 // and no network activity takes place.
141 // This test also uses a custom callback function that replaces all
142 // misspelled words with ones suggested by the service so this test can compare
143 // the corrected text with the expected results. (If there are not any
144 // misspelled words, |corrected_text| should be equal to |request_text|.)
145 using Redirects = std::vector<
146     std::pair<net::RedirectInfo, network::mojom::URLResponseHeadPtr>>;
147 
TEST_P(SpellingServiceClientTest,RequestTextCheck)148 TEST_P(SpellingServiceClientTest, RequestTextCheck) {
149   auto test_case = GetParam();
150 
151   PrefService* pref = profile_.GetPrefs();
152   pref->SetBoolean(spellcheck::prefs::kSpellCheckEnable, true);
153   pref->SetBoolean(spellcheck::prefs::kSpellCheckUseSpellingService, true);
154 
155   client_.test_url_loader_factory()->ClearResponses();
156   net::HttpStatusCode http_status = test_case.response_status;
157 
158   auto head = network::mojom::URLResponseHead::New();
159   std::string headers(base::StringPrintf(
160       "HTTP/1.1 %d %s\nContent-type: application/json\n\n",
161       static_cast<int>(http_status), net::GetHttpReasonPhrase(http_status)));
162   head->headers = base::MakeRefCounted<net::HttpResponseHeaders>(
163       net::HttpUtil::AssembleRawHeaders(headers));
164   head->mime_type = "application/json";
165 
166   network::URLLoaderCompletionStatus status;
167   status.decoded_body_length = test_case.response_data.size();
168   GURL expected_request_url = client_.BuildEndpointUrl(test_case.request_type);
169   client_.test_url_loader_factory()->AddResponse(
170       expected_request_url, std::move(head), test_case.response_data, status,
171       Redirects(),
172       network::TestURLLoaderFactory::ResponseProduceFlags::
173           kSendHeadersOnNetworkError);
174 
175   net::HttpRequestHeaders intercepted_headers;
176   std::string intercepted_body;
177   GURL requested_url;
178   client_.test_url_loader_factory()->SetInterceptor(
179       base::BindLambdaForTesting([&](const network::ResourceRequest& request) {
180         intercepted_headers = request.headers;
181         intercepted_body = network::GetUploadData(request);
182         requested_url = request.url;
183       }));
184   client_.SetExpectedTextCheckResult(test_case.success,
185                                      test_case.sanitized_request_text,
186                                      test_case.corrected_text);
187 
188   base::ListValue dictionary;
189   dictionary.AppendString(test_case.language);
190   pref->Set(spellcheck::prefs::kSpellCheckDictionaries, dictionary);
191 
192   client_.RequestTextCheck(
193       &profile_, test_case.request_type,
194       base::WideToUTF16(test_case.request_text),
195       base::BindOnce(&SpellingServiceClientTest::OnTextCheckComplete,
196                      base::Unretained(this), 0));
197   task_environment_.RunUntilIdle();
198 
199   // Verify that the expected endpoint was hit (REST vs RPC).
200   ASSERT_EQ(requested_url.path(), expected_request_url.path());
201 
202   // Verify the request content type was JSON. (The Spelling service returns
203   // an internal server error when this content type is not JSON.)
204   std::string request_content_type;
205   ASSERT_TRUE(intercepted_headers.GetHeader(
206       net::HttpRequestHeaders::kContentType, &request_content_type));
207   EXPECT_EQ("application/json", request_content_type);
208 
209   // Parse the JSON sent to the service, and verify its parameters.
210   std::unique_ptr<base::DictionaryValue> value(
211       static_cast<base::DictionaryValue*>(
212           base::JSONReader::ReadDeprecated(intercepted_body,
213                                            base::JSON_ALLOW_TRAILING_COMMAS)
214               .release()));
215   ASSERT_TRUE(value.get());
216 
217   std::string method;
218   EXPECT_FALSE(value->GetString("method", &method));
219   std::string version;
220   EXPECT_FALSE(value->GetString("apiVersion", &version));
221   std::string sanitized_text;
222   EXPECT_TRUE(value->GetString("text", &sanitized_text));
223   EXPECT_EQ(test_case.sanitized_request_text, sanitized_text);
224   std::string language;
225   EXPECT_TRUE(value->GetString("language", &language));
226   std::string expected_language =
227       test_case.language.empty() ? std::string("en") : test_case.language;
228   EXPECT_EQ(expected_language, language);
229   std::string expected_country;
230   ASSERT_TRUE(GetExpectedCountry(language, &expected_country));
231   std::string country;
232   EXPECT_TRUE(value->GetString("originCountry", &country));
233   EXPECT_EQ(expected_country, country);
234 }
235 
236 INSTANTIATE_TEST_SUITE_P(
237     SpellingService,
238     SpellingServiceClientTest,
239     testing::Values(
240         // Test cases for the REST endpoint
241         SpellingServiceTestCase{
242             L"",
243             "",
244             SpellingServiceClient::SUGGEST,
245             net::HttpStatusCode(500),
246             "",
247             false,
248             "",
249             "af",
250         },
251         SpellingServiceTestCase{
252             L"chromebook",
253             "chromebook",
254             SpellingServiceClient::SUGGEST,
255             net::HttpStatusCode(200),
256             "{}",
257             true,
258             "chromebook",
259             "af",
260         },
261         SpellingServiceTestCase{
262             L"chrombook",
263             "chrombook",
264             SpellingServiceClient::SUGGEST,
265             net::HttpStatusCode(200),
266             "{\n"
267             "  \"spellingCheckResponse\": {\n"
268             "    \"misspellings\": [{\n"
269             "      \"charStart\": 0,\n"
270             "      \"charLength\": 9,\n"
271             "      \"suggestions\": [{ \"suggestion\": \"chromebook\" }],\n"
272             "      \"canAutoCorrect\": false\n"
273             "    }]\n"
274             "  }\n"
275             "}",
276             true,
277             "chromebook",
278             "af",
279         },
280         SpellingServiceTestCase{
281             L"",
282             "",
283             SpellingServiceClient::SPELLCHECK,
284             net::HttpStatusCode(500),
285             "",
286             false,
287             "",
288             "en",
289         },
290         SpellingServiceTestCase{
291             L"I have been to USA.",
292             "I have been to USA.",
293             SpellingServiceClient::SPELLCHECK,
294             net::HttpStatusCode(200),
295             "{}",
296             true,
297             "I have been to USA.",
298             "en",
299         },
300         SpellingServiceTestCase{
301             L"I have bean to USA.",
302             "I have bean to USA.",
303             SpellingServiceClient::SPELLCHECK,
304             net::HttpStatusCode(200),
305             "{\n"
306             "  \"spellingCheckResponse\": {\n"
307             "    \"misspellings\": [{\n"
308             "      \"charStart\": 7,\n"
309             "      \"charLength\": 4,\n"
310             "      \"suggestions\": [{ \"suggestion\": \"been\" }],\n"
311             "      \"canAutoCorrect\": false\n"
312             "    }]\n"
313             "  }\n"
314             "}",
315             true,
316             "I have been to USA.",
317             "en",
318         },
319         SpellingServiceTestCase{
320             L"I\x2019mattheIn'n'Out.",
321             "I'mattheIn'n'Out.",
322             SpellingServiceClient::SPELLCHECK,
323             net::HttpStatusCode(200),
324             "{\n"
325             "  \"spellingCheckResponse\": {\n"
326             "    \"misspellings\": [{\n"
327             "      \"charStart\": 0,\n"
328             "      \"charLength\": 16,\n"
329             "      \"suggestions\":"
330             " [{ \"suggestion\": \"I'm at the In'N'Out\" }],\n"
331             "      \"canAutoCorrect\": false\n"
332             "    }]\n"
333             "  }\n"
334             "}",
335             true,
336             "I'm at the In'N'Out.",
337             "en",
338         }));
339 
340 // Verify that SpellingServiceClient::IsAvailable() returns true only when it
341 // can send suggest requests or spellcheck requests.
TEST_F(SpellingServiceClientTest,AvailableServices)342 TEST_F(SpellingServiceClientTest, AvailableServices) {
343   const SpellingServiceClient::ServiceType kSuggest =
344       SpellingServiceClient::SUGGEST;
345   const SpellingServiceClient::ServiceType kSpellcheck =
346       SpellingServiceClient::SPELLCHECK;
347 
348   // When a user disables spellchecking or prevent using the Spelling service,
349   // this function should return false both for suggestions and for spellcheck.
350   PrefService* pref = profile_.GetPrefs();
351   pref->SetBoolean(spellcheck::prefs::kSpellCheckEnable, false);
352   pref->SetBoolean(spellcheck::prefs::kSpellCheckUseSpellingService, false);
353   EXPECT_FALSE(client_.IsAvailable(&profile_, kSuggest));
354   EXPECT_FALSE(client_.IsAvailable(&profile_, kSpellcheck));
355 
356   pref->SetBoolean(spellcheck::prefs::kSpellCheckEnable, true);
357   pref->SetBoolean(spellcheck::prefs::kSpellCheckUseSpellingService, true);
358 
359   // For locales supported by the SpellCheck service, this function returns
360   // false for suggestions and true for spellcheck. (The comment in
361   // SpellingServiceClient::IsAvailable() describes why this function returns
362   // false for suggestions.) If there is no language set, then we
363   // do not allow any remote.
364   pref->Set(spellcheck::prefs::kSpellCheckDictionaries, base::ListValue());
365 
366   EXPECT_FALSE(client_.IsAvailable(&profile_, kSuggest));
367   EXPECT_FALSE(client_.IsAvailable(&profile_, kSpellcheck));
368 
369   static const char* kSupported[] = {
370       "en-AU", "en-CA", "en-GB", "en-US", "da-DK", "es-ES",
371   };
372   // If spellcheck is allowed, then suggest is not since spellcheck is a
373   // superset of suggest.
374   for (size_t i = 0; i < base::size(kSupported); ++i) {
375     base::ListValue dictionary;
376     dictionary.AppendString(kSupported[i]);
377     pref->Set(spellcheck::prefs::kSpellCheckDictionaries, dictionary);
378 
379     EXPECT_FALSE(client_.IsAvailable(&profile_, kSuggest));
380     EXPECT_TRUE(client_.IsAvailable(&profile_, kSpellcheck));
381   }
382 
383   // This function returns true for suggestions for all and false for
384   // spellcheck for unsupported locales.
385   static const char* kUnsupported[] = {
386       "af-ZA", "bg-BG", "ca-ES", "cs-CZ", "de-DE", "el-GR", "et-EE", "fo-FO",
387       "fr-FR", "he-IL", "hi-IN", "hr-HR", "hu-HU", "id-ID", "it-IT", "lt-LT",
388       "lv-LV", "nb-NO", "nl-NL", "pl-PL", "pt-BR", "pt-PT", "ro-RO", "ru-RU",
389       "sk-SK", "sl-SI", "sh",    "sr",    "sv-SE", "tr-TR", "uk-UA", "vi-VN",
390   };
391   for (size_t i = 0; i < base::size(kUnsupported); ++i) {
392     SCOPED_TRACE(std::string("Expected language ") + kUnsupported[i]);
393     base::ListValue dictionary;
394     dictionary.AppendString(kUnsupported[i]);
395     pref->Set(spellcheck::prefs::kSpellCheckDictionaries, dictionary);
396 
397     EXPECT_TRUE(client_.IsAvailable(&profile_, kSuggest));
398     EXPECT_FALSE(client_.IsAvailable(&profile_, kSpellcheck));
399   }
400 }
401 
402 // Verify that an error in JSON response from spelling service will result in
403 // ParseResponse returning false.
TEST_F(SpellingServiceClientTest,ResponseErrorTest)404 TEST_F(SpellingServiceClientTest, ResponseErrorTest) {
405   EXPECT_TRUE(client_.ParseResponseSuccess("{\"result\": {}}"));
406   EXPECT_FALSE(client_.ParseResponseSuccess("{\"error\": {}}"));
407 }
408