Ben Murdoch | c5cede9 | 2014-04-10 11:22:14 +0100 | [diff] [blame] | 1 | // Copyright 2014 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 "extensions/common/message_bundle.h" |
| 6 | |
| 7 | #include <string> |
| 8 | #include <vector> |
| 9 | |
| 10 | #include "base/i18n/rtl.h" |
| 11 | #include "base/memory/linked_ptr.h" |
| 12 | #include "base/memory/scoped_ptr.h" |
| 13 | #include "base/strings/string_util.h" |
| 14 | #include "base/strings/utf_string_conversions.h" |
| 15 | #include "base/values.h" |
| 16 | #include "extensions/common/error_utils.h" |
| 17 | #include "extensions/common/extension_l10n_util.h" |
| 18 | #include "extensions/common/manifest_constants.h" |
| 19 | #include "testing/gtest/include/gtest/gtest.h" |
| 20 | |
| 21 | namespace extensions { |
| 22 | |
| 23 | namespace errors = manifest_errors; |
| 24 | |
| 25 | class MessageBundleTest : public testing::Test { |
| 26 | protected: |
| 27 | enum BadDictionary { |
| 28 | INVALID_NAME, |
| 29 | NAME_NOT_A_TREE, |
| 30 | EMPTY_NAME_TREE, |
| 31 | MISSING_MESSAGE, |
| 32 | PLACEHOLDER_NOT_A_TREE, |
| 33 | EMPTY_PLACEHOLDER_TREE, |
| 34 | CONTENT_MISSING, |
| 35 | MESSAGE_PLACEHOLDER_DOESNT_MATCH, |
| 36 | }; |
| 37 | |
| 38 | // Helper method for dictionary building. |
| 39 | void SetDictionary(const std::string& name, |
| 40 | base::DictionaryValue* subtree, |
| 41 | base::DictionaryValue* target) { |
| 42 | target->Set(name, static_cast<base::Value*>(subtree)); |
| 43 | } |
| 44 | |
| 45 | void CreateContentTree(const std::string& name, |
| 46 | const std::string& content, |
| 47 | base::DictionaryValue* dict) { |
| 48 | base::DictionaryValue* content_tree = new base::DictionaryValue; |
| 49 | content_tree->SetString(MessageBundle::kContentKey, content); |
| 50 | SetDictionary(name, content_tree, dict); |
| 51 | } |
| 52 | |
| 53 | void CreatePlaceholdersTree(base::DictionaryValue* dict) { |
| 54 | base::DictionaryValue* placeholders_tree = new base::DictionaryValue; |
| 55 | CreateContentTree("a", "A", placeholders_tree); |
| 56 | CreateContentTree("b", "B", placeholders_tree); |
| 57 | CreateContentTree("c", "C", placeholders_tree); |
| 58 | SetDictionary(MessageBundle::kPlaceholdersKey, |
| 59 | placeholders_tree, |
| 60 | dict); |
| 61 | } |
| 62 | |
| 63 | void CreateMessageTree(const std::string& name, |
| 64 | const std::string& message, |
| 65 | bool create_placeholder_subtree, |
| 66 | base::DictionaryValue* dict) { |
| 67 | base::DictionaryValue* message_tree = new base::DictionaryValue; |
| 68 | if (create_placeholder_subtree) |
| 69 | CreatePlaceholdersTree(message_tree); |
| 70 | message_tree->SetString(MessageBundle::kMessageKey, message); |
| 71 | SetDictionary(name, message_tree, dict); |
| 72 | } |
| 73 | |
| 74 | // Caller owns the memory. |
| 75 | base::DictionaryValue* CreateGoodDictionary() { |
| 76 | base::DictionaryValue* dict = new base::DictionaryValue; |
| 77 | CreateMessageTree("n1", "message1 $a$ $b$", true, dict); |
| 78 | CreateMessageTree("n2", "message2 $c$", true, dict); |
| 79 | CreateMessageTree("n3", "message3", false, dict); |
| 80 | return dict; |
| 81 | } |
| 82 | |
| 83 | // Caller owns the memory. |
| 84 | base::DictionaryValue* CreateBadDictionary(enum BadDictionary what_is_bad) { |
| 85 | base::DictionaryValue* dict = CreateGoodDictionary(); |
| 86 | // Now remove/break things. |
| 87 | switch (what_is_bad) { |
| 88 | case INVALID_NAME: |
| 89 | CreateMessageTree("n 5", "nevermind", false, dict); |
| 90 | break; |
| 91 | case NAME_NOT_A_TREE: |
| 92 | dict->SetString("n4", "whatever"); |
| 93 | break; |
| 94 | case EMPTY_NAME_TREE: { |
| 95 | base::DictionaryValue* empty_tree = new base::DictionaryValue; |
| 96 | SetDictionary("n4", empty_tree, dict); |
| 97 | } |
| 98 | break; |
| 99 | case MISSING_MESSAGE: |
| 100 | dict->Remove("n1.message", NULL); |
| 101 | break; |
| 102 | case PLACEHOLDER_NOT_A_TREE: |
| 103 | dict->SetString("n1.placeholders", "whatever"); |
| 104 | break; |
| 105 | case EMPTY_PLACEHOLDER_TREE: { |
| 106 | base::DictionaryValue* empty_tree = new base::DictionaryValue; |
| 107 | SetDictionary("n1.placeholders", empty_tree, dict); |
| 108 | } |
| 109 | break; |
| 110 | case CONTENT_MISSING: |
| 111 | dict->Remove("n1.placeholders.a.content", NULL); |
| 112 | break; |
| 113 | case MESSAGE_PLACEHOLDER_DOESNT_MATCH: |
| 114 | base::DictionaryValue* value; |
| 115 | dict->Remove("n1.placeholders.a", NULL); |
| 116 | dict->GetDictionary("n1.placeholders", &value); |
| 117 | CreateContentTree("x", "X", value); |
| 118 | break; |
| 119 | } |
| 120 | |
| 121 | return dict; |
| 122 | } |
| 123 | |
| 124 | unsigned int ReservedMessagesCount() { |
| 125 | // Update when adding new reserved messages. |
| 126 | return 5U; |
| 127 | } |
| 128 | |
| 129 | void CheckReservedMessages(MessageBundle* handler) { |
| 130 | std::string ui_locale = extension_l10n_util::CurrentLocaleOrDefault(); |
| 131 | EXPECT_EQ(ui_locale, |
| 132 | handler->GetL10nMessage(MessageBundle::kUILocaleKey)); |
| 133 | |
| 134 | std::string text_dir = "ltr"; |
| 135 | if (base::i18n::GetTextDirectionForLocale(ui_locale.c_str()) == |
| 136 | base::i18n::RIGHT_TO_LEFT) |
| 137 | text_dir = "rtl"; |
| 138 | |
| 139 | EXPECT_EQ(text_dir, handler->GetL10nMessage( |
| 140 | MessageBundle::kBidiDirectionKey)); |
| 141 | } |
| 142 | |
| 143 | bool AppendReservedMessages(const std::string& application_locale) { |
| 144 | std::string error; |
| 145 | return handler_->AppendReservedMessagesForLocale( |
| 146 | application_locale, &error); |
| 147 | } |
| 148 | |
| 149 | std::string CreateMessageBundle() { |
| 150 | std::string error; |
| 151 | handler_.reset(MessageBundle::Create(catalogs_, &error)); |
| 152 | |
| 153 | return error; |
| 154 | } |
| 155 | |
| 156 | void ClearDictionary() { |
| 157 | handler_->dictionary_.clear(); |
| 158 | } |
| 159 | |
| 160 | scoped_ptr<MessageBundle> handler_; |
| 161 | std::vector<linked_ptr<base::DictionaryValue> > catalogs_; |
| 162 | }; |
| 163 | |
| 164 | TEST_F(MessageBundleTest, ReservedMessagesCount) { |
| 165 | ASSERT_EQ(5U, ReservedMessagesCount()); |
| 166 | } |
| 167 | |
| 168 | TEST_F(MessageBundleTest, InitEmptyDictionaries) { |
| 169 | CreateMessageBundle(); |
| 170 | EXPECT_TRUE(handler_.get() != NULL); |
| 171 | EXPECT_EQ(0U + ReservedMessagesCount(), handler_->size()); |
| 172 | CheckReservedMessages(handler_.get()); |
| 173 | } |
| 174 | |
| 175 | TEST_F(MessageBundleTest, InitGoodDefaultDict) { |
| 176 | catalogs_.push_back( |
| 177 | linked_ptr<base::DictionaryValue>(CreateGoodDictionary())); |
| 178 | CreateMessageBundle(); |
| 179 | |
| 180 | EXPECT_TRUE(handler_.get() != NULL); |
| 181 | EXPECT_EQ(3U + ReservedMessagesCount(), handler_->size()); |
| 182 | |
| 183 | EXPECT_EQ("message1 A B", handler_->GetL10nMessage("n1")); |
| 184 | EXPECT_EQ("message2 C", handler_->GetL10nMessage("n2")); |
| 185 | EXPECT_EQ("message3", handler_->GetL10nMessage("n3")); |
| 186 | CheckReservedMessages(handler_.get()); |
| 187 | } |
| 188 | |
| 189 | TEST_F(MessageBundleTest, InitAppDictConsultedFirst) { |
| 190 | catalogs_.push_back( |
| 191 | linked_ptr<base::DictionaryValue>(CreateGoodDictionary())); |
| 192 | catalogs_.push_back( |
| 193 | linked_ptr<base::DictionaryValue>(CreateGoodDictionary())); |
| 194 | |
| 195 | base::DictionaryValue* app_dict = catalogs_[0].get(); |
| 196 | // Flip placeholders in message of n1 tree. |
| 197 | app_dict->SetString("n1.message", "message1 $b$ $a$"); |
| 198 | // Remove one message from app dict. |
| 199 | app_dict->Remove("n2", NULL); |
| 200 | // Replace n3 with N3. |
| 201 | app_dict->Remove("n3", NULL); |
| 202 | CreateMessageTree("N3", "message3_app_dict", false, app_dict); |
| 203 | |
| 204 | CreateMessageBundle(); |
| 205 | |
| 206 | EXPECT_TRUE(handler_.get() != NULL); |
| 207 | EXPECT_EQ(3U + ReservedMessagesCount(), handler_->size()); |
| 208 | |
| 209 | EXPECT_EQ("message1 B A", handler_->GetL10nMessage("n1")); |
| 210 | EXPECT_EQ("message2 C", handler_->GetL10nMessage("n2")); |
| 211 | EXPECT_EQ("message3_app_dict", handler_->GetL10nMessage("n3")); |
| 212 | CheckReservedMessages(handler_.get()); |
| 213 | } |
| 214 | |
| 215 | TEST_F(MessageBundleTest, InitBadAppDict) { |
| 216 | catalogs_.push_back( |
| 217 | linked_ptr<base::DictionaryValue>(CreateBadDictionary(INVALID_NAME))); |
| 218 | catalogs_.push_back( |
| 219 | linked_ptr<base::DictionaryValue>(CreateGoodDictionary())); |
| 220 | |
| 221 | std::string error = CreateMessageBundle(); |
| 222 | |
| 223 | EXPECT_TRUE(handler_.get() == NULL); |
| 224 | EXPECT_EQ("Name of a key \"n 5\" is invalid. Only ASCII [a-z], " |
| 225 | "[A-Z], [0-9] and \"_\" are allowed.", error); |
| 226 | |
| 227 | catalogs_[0].reset(CreateBadDictionary(NAME_NOT_A_TREE)); |
| 228 | handler_.reset(MessageBundle::Create(catalogs_, &error)); |
| 229 | EXPECT_TRUE(handler_.get() == NULL); |
| 230 | EXPECT_EQ("Not a valid tree for key n4.", error); |
| 231 | |
| 232 | catalogs_[0].reset(CreateBadDictionary(EMPTY_NAME_TREE)); |
| 233 | handler_.reset(MessageBundle::Create(catalogs_, &error)); |
| 234 | EXPECT_TRUE(handler_.get() == NULL); |
| 235 | EXPECT_EQ("There is no \"message\" element for key n4.", error); |
| 236 | |
| 237 | catalogs_[0].reset(CreateBadDictionary(MISSING_MESSAGE)); |
| 238 | handler_.reset(MessageBundle::Create(catalogs_, &error)); |
| 239 | EXPECT_TRUE(handler_.get() == NULL); |
| 240 | EXPECT_EQ("There is no \"message\" element for key n1.", error); |
| 241 | |
| 242 | catalogs_[0].reset(CreateBadDictionary(PLACEHOLDER_NOT_A_TREE)); |
| 243 | handler_.reset(MessageBundle::Create(catalogs_, &error)); |
| 244 | EXPECT_TRUE(handler_.get() == NULL); |
| 245 | EXPECT_EQ("Not a valid \"placeholders\" element for key n1.", error); |
| 246 | |
| 247 | catalogs_[0].reset(CreateBadDictionary(EMPTY_PLACEHOLDER_TREE)); |
| 248 | handler_.reset(MessageBundle::Create(catalogs_, &error)); |
| 249 | EXPECT_TRUE(handler_.get() == NULL); |
| 250 | EXPECT_EQ("Variable $a$ used but not defined.", error); |
| 251 | |
| 252 | catalogs_[0].reset(CreateBadDictionary(CONTENT_MISSING)); |
| 253 | handler_.reset(MessageBundle::Create(catalogs_, &error)); |
| 254 | EXPECT_TRUE(handler_.get() == NULL); |
| 255 | EXPECT_EQ("Invalid \"content\" element for key n1.", error); |
| 256 | |
| 257 | catalogs_[0].reset(CreateBadDictionary(MESSAGE_PLACEHOLDER_DOESNT_MATCH)); |
| 258 | handler_.reset(MessageBundle::Create(catalogs_, &error)); |
| 259 | EXPECT_TRUE(handler_.get() == NULL); |
| 260 | EXPECT_EQ("Variable $a$ used but not defined.", error); |
| 261 | } |
| 262 | |
| 263 | TEST_F(MessageBundleTest, ReservedMessagesOverrideDeveloperMessages) { |
| 264 | catalogs_.push_back( |
| 265 | linked_ptr<base::DictionaryValue>(CreateGoodDictionary())); |
| 266 | |
| 267 | base::DictionaryValue* dict = catalogs_[0].get(); |
| 268 | CreateMessageTree(MessageBundle::kUILocaleKey, "x", false, dict); |
| 269 | |
| 270 | std::string error = CreateMessageBundle(); |
| 271 | |
| 272 | EXPECT_TRUE(handler_.get() == NULL); |
| 273 | std::string expected_error = ErrorUtils::FormatErrorMessage( |
| 274 | errors::kReservedMessageFound, MessageBundle::kUILocaleKey); |
| 275 | EXPECT_EQ(expected_error, error); |
| 276 | } |
| 277 | |
| 278 | TEST_F(MessageBundleTest, AppendReservedMessagesForLTR) { |
| 279 | CreateMessageBundle(); |
| 280 | |
| 281 | ASSERT_TRUE(handler_.get() != NULL); |
| 282 | ClearDictionary(); |
| 283 | ASSERT_TRUE(AppendReservedMessages("en_US")); |
| 284 | |
| 285 | EXPECT_EQ("en_US", |
| 286 | handler_->GetL10nMessage(MessageBundle::kUILocaleKey)); |
| 287 | EXPECT_EQ("ltr", handler_->GetL10nMessage( |
| 288 | MessageBundle::kBidiDirectionKey)); |
| 289 | EXPECT_EQ("rtl", handler_->GetL10nMessage( |
| 290 | MessageBundle::kBidiReversedDirectionKey)); |
| 291 | EXPECT_EQ("left", handler_->GetL10nMessage( |
| 292 | MessageBundle::kBidiStartEdgeKey)); |
| 293 | EXPECT_EQ("right", handler_->GetL10nMessage( |
| 294 | MessageBundle::kBidiEndEdgeKey)); |
| 295 | } |
| 296 | |
| 297 | TEST_F(MessageBundleTest, AppendReservedMessagesForRTL) { |
| 298 | CreateMessageBundle(); |
| 299 | |
| 300 | ASSERT_TRUE(handler_.get() != NULL); |
| 301 | ClearDictionary(); |
| 302 | ASSERT_TRUE(AppendReservedMessages("he")); |
| 303 | |
| 304 | EXPECT_EQ("he", |
| 305 | handler_->GetL10nMessage(MessageBundle::kUILocaleKey)); |
| 306 | EXPECT_EQ("rtl", handler_->GetL10nMessage( |
| 307 | MessageBundle::kBidiDirectionKey)); |
| 308 | EXPECT_EQ("ltr", handler_->GetL10nMessage( |
| 309 | MessageBundle::kBidiReversedDirectionKey)); |
| 310 | EXPECT_EQ("right", handler_->GetL10nMessage( |
| 311 | MessageBundle::kBidiStartEdgeKey)); |
| 312 | EXPECT_EQ("left", handler_->GetL10nMessage( |
| 313 | MessageBundle::kBidiEndEdgeKey)); |
| 314 | } |
| 315 | |
| 316 | TEST_F(MessageBundleTest, IsValidNameCheckValidCharacters) { |
| 317 | EXPECT_TRUE(MessageBundle::IsValidName(std::string("a__BV_9"))); |
| 318 | EXPECT_TRUE(MessageBundle::IsValidName(std::string("@@a__BV_9"))); |
| 319 | EXPECT_FALSE(MessageBundle::IsValidName(std::string("$a__BV_9$"))); |
| 320 | EXPECT_FALSE(MessageBundle::IsValidName(std::string("a-BV-9"))); |
| 321 | EXPECT_FALSE(MessageBundle::IsValidName(std::string("a#BV!9"))); |
| 322 | EXPECT_FALSE(MessageBundle::IsValidName(std::string("a<b"))); |
| 323 | } |
| 324 | |
| 325 | struct ReplaceVariables { |
| 326 | const char* original; |
| 327 | const char* result; |
| 328 | const char* error; |
| 329 | const char* begin_delimiter; |
| 330 | const char* end_delimiter; |
| 331 | bool pass; |
| 332 | }; |
| 333 | |
| 334 | TEST(MessageBundle, ReplaceMessagesInText) { |
| 335 | const char* kMessageBegin = MessageBundle::kMessageBegin; |
| 336 | const char* kMessageEnd = MessageBundle::kMessageEnd; |
| 337 | const char* kPlaceholderBegin = MessageBundle::kPlaceholderBegin; |
| 338 | const char* kPlaceholderEnd = MessageBundle::kPlaceholderEnd; |
| 339 | |
| 340 | static ReplaceVariables test_cases[] = { |
| 341 | // Message replacement. |
| 342 | { "This is __MSG_siMPle__ message", "This is simple message", |
| 343 | "", kMessageBegin, kMessageEnd, true }, |
| 344 | { "This is __MSG_", "This is __MSG_", |
| 345 | "", kMessageBegin, kMessageEnd, true }, |
| 346 | { "This is __MSG__simple__ message", "This is __MSG__simple__ message", |
| 347 | "Variable __MSG__simple__ used but not defined.", |
| 348 | kMessageBegin, kMessageEnd, false }, |
| 349 | { "__MSG_LoNg__", "A pretty long replacement", |
| 350 | "", kMessageBegin, kMessageEnd, true }, |
| 351 | { "A __MSG_SimpLE__MSG_ a", "A simpleMSG_ a", |
| 352 | "", kMessageBegin, kMessageEnd, true }, |
| 353 | { "A __MSG_simple__MSG_long__", "A simpleMSG_long__", |
| 354 | "", kMessageBegin, kMessageEnd, true }, |
| 355 | { "A __MSG_simple____MSG_long__", "A simpleA pretty long replacement", |
| 356 | "", kMessageBegin, kMessageEnd, true }, |
| 357 | { "__MSG_d1g1ts_are_ok__", "I are d1g1t", |
| 358 | "", kMessageBegin, kMessageEnd, true }, |
| 359 | // Placeholder replacement. |
| 360 | { "This is $sImpLe$ message", "This is simple message", |
| 361 | "", kPlaceholderBegin, kPlaceholderEnd, true }, |
| 362 | { "This is $", "This is $", |
| 363 | "", kPlaceholderBegin, kPlaceholderEnd, true }, |
| 364 | { "This is $$sIMPle$ message", "This is $simple message", |
| 365 | "", kPlaceholderBegin, kPlaceholderEnd, true }, |
| 366 | { "$LONG_V$", "A pretty long replacement", |
| 367 | "", kPlaceholderBegin, kPlaceholderEnd, true }, |
| 368 | { "A $simple$$ a", "A simple$ a", |
| 369 | "", kPlaceholderBegin, kPlaceholderEnd, true }, |
| 370 | { "A $simple$long_v$", "A simplelong_v$", |
| 371 | "", kPlaceholderBegin, kPlaceholderEnd, true }, |
| 372 | { "A $simple$$long_v$", "A simpleA pretty long replacement", |
| 373 | "", kPlaceholderBegin, kPlaceholderEnd, true }, |
| 374 | { "This is $bad name$", "This is $bad name$", |
| 375 | "", kPlaceholderBegin, kPlaceholderEnd, true }, |
| 376 | { "This is $missing$", "This is $missing$", |
| 377 | "Variable $missing$ used but not defined.", |
| 378 | kPlaceholderBegin, kPlaceholderEnd, false }, |
| 379 | }; |
| 380 | |
| 381 | MessageBundle::SubstitutionMap messages; |
| 382 | messages.insert(std::make_pair("simple", "simple")); |
| 383 | messages.insert(std::make_pair("long", "A pretty long replacement")); |
| 384 | messages.insert(std::make_pair("long_v", "A pretty long replacement")); |
| 385 | messages.insert(std::make_pair("bad name", "Doesn't matter")); |
| 386 | messages.insert(std::make_pair("d1g1ts_are_ok", "I are d1g1t")); |
| 387 | |
| 388 | for (size_t i = 0; i < arraysize(test_cases); ++i) { |
| 389 | std::string text = test_cases[i].original; |
| 390 | std::string error; |
| 391 | EXPECT_EQ(test_cases[i].pass, |
| 392 | MessageBundle::ReplaceVariables(messages, |
| 393 | test_cases[i].begin_delimiter, |
| 394 | test_cases[i].end_delimiter, |
| 395 | &text, |
| 396 | &error)); |
| 397 | EXPECT_EQ(test_cases[i].result, text); |
| 398 | } |
| 399 | } |
| 400 | |
| 401 | /////////////////////////////////////////////////////////////////////////////// |
| 402 | // |
| 403 | // Renderer helper functions test. |
| 404 | // |
| 405 | /////////////////////////////////////////////////////////////////////////////// |
| 406 | |
| 407 | TEST(GetExtensionToL10nMessagesMapTest, ReturnsTheSameObject) { |
| 408 | ExtensionToL10nMessagesMap* map1 = GetExtensionToL10nMessagesMap(); |
| 409 | ASSERT_TRUE(NULL != map1); |
| 410 | |
| 411 | ExtensionToL10nMessagesMap* map2 = GetExtensionToL10nMessagesMap(); |
| 412 | ASSERT_EQ(map1, map2); |
| 413 | } |
| 414 | |
| 415 | TEST(GetExtensionToL10nMessagesMapTest, ReturnsNullForUnknownExtensionId) { |
| 416 | const std::string extension_id("some_unique_12334212314234_id"); |
| 417 | L10nMessagesMap* map = GetL10nMessagesMap(extension_id); |
| 418 | EXPECT_TRUE(NULL == map); |
| 419 | } |
| 420 | |
| 421 | TEST(GetExtensionToL10nMessagesMapTest, ReturnsMapForKnownExtensionId) { |
| 422 | const std::string extension_id("some_unique_121212121212121_id"); |
| 423 | // Store a map for given id. |
| 424 | L10nMessagesMap messages; |
| 425 | messages.insert(std::make_pair("message_name", "message_value")); |
| 426 | (*GetExtensionToL10nMessagesMap())[extension_id] = messages; |
| 427 | |
| 428 | L10nMessagesMap* map = GetL10nMessagesMap(extension_id); |
| 429 | ASSERT_TRUE(NULL != map); |
| 430 | EXPECT_EQ(1U, map->size()); |
| 431 | EXPECT_EQ("message_value", (*map)["message_name"]); |
| 432 | } |
| 433 | |
| 434 | } // namespace extensions |