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