OLD | NEW |
1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/browser/password_manager/password_store_proxy_mac.h" | 5 #include "chrome/browser/password_manager/password_store_proxy_mac.h" |
6 | 6 |
7 #include "base/files/scoped_temp_dir.h" | 7 #include "base/files/scoped_temp_dir.h" |
8 #include "base/scoped_observer.h" | 8 #include "base/scoped_observer.h" |
9 #include "base/strings/utf_string_conversions.h" | 9 #include "base/strings/utf_string_conversions.h" |
| 10 #include "chrome/browser/password_manager/password_store_mac_internal.h" |
| 11 #include "chrome/browser/prefs/browser_prefs.h" |
| 12 #include "chrome/test/base/testing_pref_service_syncable.h" |
10 #include "components/os_crypt/os_crypt.h" | 13 #include "components/os_crypt/os_crypt.h" |
11 #include "components/password_manager/core/browser/login_database.h" | 14 #include "components/password_manager/core/browser/login_database.h" |
12 #include "components/password_manager/core/browser/password_manager_test_utils.h
" | 15 #include "components/password_manager/core/browser/password_manager_test_utils.h
" |
13 #include "components/password_manager/core/browser/password_store_consumer.h" | 16 #include "components/password_manager/core/browser/password_store_consumer.h" |
| 17 #include "components/password_manager/core/common/password_manager_pref_names.h" |
14 #include "content/public/browser/browser_thread.h" | 18 #include "content/public/browser/browser_thread.h" |
15 #include "content/public/test/test_browser_thread_bundle.h" | 19 #include "content/public/test/test_browser_thread_bundle.h" |
16 #include "crypto/mock_apple_keychain.h" | 20 #include "crypto/mock_apple_keychain.h" |
17 #include "testing/gmock/include/gmock/gmock.h" | 21 #include "testing/gmock/include/gmock/gmock.h" |
18 #include "testing/gtest/include/gtest/gtest.h" | 22 #include "testing/gtest/include/gtest/gtest.h" |
19 | 23 |
20 namespace { | 24 namespace { |
21 | 25 |
22 using autofill::PasswordForm; | 26 using autofill::PasswordForm; |
23 using content::BrowserThread; | 27 using content::BrowserThread; |
| 28 using password_manager::MigrationStatus; |
| 29 using password_manager::PasswordStoreChange; |
| 30 using password_manager::PasswordStoreChangeList; |
24 using testing::_; | 31 using testing::_; |
25 using testing::ElementsAre; | 32 using testing::ElementsAre; |
26 using testing::IsEmpty; | 33 using testing::IsEmpty; |
27 using testing::Pointee; | 34 using testing::Pointee; |
28 | 35 |
29 ACTION(QuitUIMessageLoop) { | 36 ACTION(QuitUIMessageLoop) { |
30 DCHECK_CURRENTLY_ON(BrowserThread::UI); | 37 DCHECK_CURRENTLY_ON(BrowserThread::UI); |
31 base::MessageLoop::current()->Quit(); | 38 base::MessageLoop::current()->Quit(); |
32 } | 39 } |
33 | 40 |
| 41 // Returns a change list corresponding to |form| being added. |
| 42 PasswordStoreChangeList AddChangeForForm(const PasswordForm& form) { |
| 43 return PasswordStoreChangeList( |
| 44 1, PasswordStoreChange(PasswordStoreChange::ADD, form)); |
| 45 } |
| 46 |
34 class MockPasswordStoreConsumer | 47 class MockPasswordStoreConsumer |
35 : public password_manager::PasswordStoreConsumer { | 48 : public password_manager::PasswordStoreConsumer { |
36 public: | 49 public: |
37 MOCK_METHOD1(OnGetPasswordStoreResultsConstRef, | 50 MOCK_METHOD1(OnGetPasswordStoreResultsConstRef, |
38 void(const std::vector<PasswordForm*>&)); | 51 void(const std::vector<PasswordForm*>&)); |
39 | 52 |
40 // GMock cannot mock methods with move-only args. | 53 // GMock cannot mock methods with move-only args. |
41 void OnGetPasswordStoreResults(ScopedVector<PasswordForm> results) override { | 54 void OnGetPasswordStoreResults(ScopedVector<PasswordForm> results) override { |
42 OnGetPasswordStoreResultsConstRef(results.get()); | 55 OnGetPasswordStoreResultsConstRef(results.get()); |
43 } | 56 } |
(...skipping 19 matching lines...) Expand all Loading... |
63 BadLoginDatabase() : password_manager::LoginDatabase(base::FilePath()) {} | 76 BadLoginDatabase() : password_manager::LoginDatabase(base::FilePath()) {} |
64 ~BadLoginDatabase() override {} | 77 ~BadLoginDatabase() override {} |
65 | 78 |
66 // LoginDatabase: | 79 // LoginDatabase: |
67 bool Init() override { return false; } | 80 bool Init() override { return false; } |
68 | 81 |
69 private: | 82 private: |
70 DISALLOW_COPY_AND_ASSIGN(BadLoginDatabase); | 83 DISALLOW_COPY_AND_ASSIGN(BadLoginDatabase); |
71 }; | 84 }; |
72 | 85 |
73 class PasswordStoreProxyMacTest : public testing::Test { | 86 class PasswordStoreProxyMacTest |
| 87 : public testing::TestWithParam<MigrationStatus> { |
74 public: | 88 public: |
| 89 PasswordStoreProxyMacTest(); |
| 90 ~PasswordStoreProxyMacTest() override; |
| 91 |
75 void SetUp() override; | 92 void SetUp() override; |
76 void TearDown() override; | 93 void TearDown() override; |
77 | 94 |
78 void CreateAndInitPasswordStore( | 95 void CreateAndInitPasswordStore( |
79 scoped_ptr<password_manager::LoginDatabase> login_db); | 96 scoped_ptr<password_manager::LoginDatabase> login_db); |
80 | 97 |
81 void ClosePasswordStore(); | 98 void ClosePasswordStore(); |
82 | 99 |
83 // Do a store-level query to wait for all the previously enqueued operations | 100 // Do a store-level query to wait for all the previously enqueued operations |
84 // to finish. | 101 // to finish. |
(...skipping 14 matching lines...) Expand all Loading... |
99 return store_->login_metadata_db(); | 116 return store_->login_metadata_db(); |
100 } | 117 } |
101 | 118 |
102 PasswordStoreProxyMac* store() { return store_.get(); } | 119 PasswordStoreProxyMac* store() { return store_.get(); } |
103 | 120 |
104 protected: | 121 protected: |
105 content::TestBrowserThreadBundle ui_thread_; | 122 content::TestBrowserThreadBundle ui_thread_; |
106 | 123 |
107 base::ScopedTempDir db_dir_; | 124 base::ScopedTempDir db_dir_; |
108 scoped_refptr<PasswordStoreProxyMac> store_; | 125 scoped_refptr<PasswordStoreProxyMac> store_; |
| 126 TestingPrefServiceSyncable testing_prefs_; |
109 }; | 127 }; |
110 | 128 |
111 void PasswordStoreProxyMacTest::SetUp() { | 129 PasswordStoreProxyMacTest::PasswordStoreProxyMacTest() { |
112 ASSERT_TRUE(db_dir_.CreateUniqueTempDir()); | 130 EXPECT_TRUE(db_dir_.CreateUniqueTempDir()); |
113 | 131 chrome::RegisterUserProfilePrefs(testing_prefs_.registry()); |
| 132 testing_prefs_.SetInteger(password_manager::prefs::kKeychainMigrationStatus, |
| 133 static_cast<int>(GetParam())); |
114 // Ensure that LoginDatabase will use the mock keychain if it needs to | 134 // Ensure that LoginDatabase will use the mock keychain if it needs to |
115 // encrypt/decrypt a password. | 135 // encrypt/decrypt a password. |
116 OSCrypt::UseMockKeychain(true); | 136 OSCrypt::UseMockKeychain(true); |
| 137 } |
| 138 |
| 139 PasswordStoreProxyMacTest::~PasswordStoreProxyMacTest() { |
| 140 } |
| 141 |
| 142 void PasswordStoreProxyMacTest::SetUp() { |
117 scoped_ptr<password_manager::LoginDatabase> login_db( | 143 scoped_ptr<password_manager::LoginDatabase> login_db( |
118 new password_manager::LoginDatabase(test_login_db_file_path())); | 144 new password_manager::LoginDatabase(test_login_db_file_path())); |
119 CreateAndInitPasswordStore(login_db.Pass()); | 145 CreateAndInitPasswordStore(login_db.Pass()); |
120 // Make sure deferred initialization is performed before some tests start | |
121 // accessing the |login_db| directly. | |
122 FinishAsyncProcessing(); | |
123 } | 146 } |
124 | 147 |
125 void PasswordStoreProxyMacTest::TearDown() { | 148 void PasswordStoreProxyMacTest::TearDown() { |
126 ClosePasswordStore(); | 149 ClosePasswordStore(); |
127 } | 150 } |
128 | 151 |
129 void PasswordStoreProxyMacTest::CreateAndInitPasswordStore( | 152 void PasswordStoreProxyMacTest::CreateAndInitPasswordStore( |
130 scoped_ptr<password_manager::LoginDatabase> login_db) { | 153 scoped_ptr<password_manager::LoginDatabase> login_db) { |
131 store_ = new PasswordStoreProxyMac( | 154 store_ = new PasswordStoreProxyMac( |
132 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::UI), | 155 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::UI), |
133 make_scoped_ptr(new crypto::MockAppleKeychain), login_db.Pass()); | 156 make_scoped_ptr(new crypto::MockAppleKeychain), login_db.Pass(), |
| 157 &testing_prefs_); |
134 ASSERT_TRUE(store_->Init(syncer::SyncableService::StartSyncFlare())); | 158 ASSERT_TRUE(store_->Init(syncer::SyncableService::StartSyncFlare())); |
135 } | 159 } |
136 | 160 |
137 void PasswordStoreProxyMacTest::ClosePasswordStore() { | 161 void PasswordStoreProxyMacTest::ClosePasswordStore() { |
138 if (!store_) | 162 if (!store_) |
139 return; | 163 return; |
140 store_->Shutdown(); | 164 store_->Shutdown(); |
141 EXPECT_FALSE(store_->GetBackgroundTaskRunner()); | 165 EXPECT_FALSE(store_->GetBackgroundTaskRunner()); |
142 base::MessageLoop::current()->RunUntilIdle(); | |
143 store_ = nullptr; | 166 store_ = nullptr; |
144 } | 167 } |
145 | 168 |
146 void PasswordStoreProxyMacTest::FinishAsyncProcessing() { | 169 void PasswordStoreProxyMacTest::FinishAsyncProcessing() { |
147 // Do a store-level query to wait for all the previously enqueued operations | 170 // Do a store-level query to wait for all the previously enqueued operations |
148 // to finish. | 171 // to finish. |
149 MockPasswordStoreConsumer consumer; | 172 MockPasswordStoreConsumer consumer; |
150 store_->GetLogins(PasswordForm(), | 173 store_->GetLogins(PasswordForm(), |
151 password_manager::PasswordStore::ALLOW_PROMPT, &consumer); | 174 password_manager::PasswordStore::ALLOW_PROMPT, &consumer); |
152 EXPECT_CALL(consumer, OnGetPasswordStoreResultsConstRef(_)) | 175 EXPECT_CALL(consumer, OnGetPasswordStoreResultsConstRef(_)) |
(...skipping 69 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
222 EXPECT_CALL(mock_observer, OnLoginsChanged(list)); | 245 EXPECT_CALL(mock_observer, OnLoginsChanged(list)); |
223 if (check_created) { | 246 if (check_created) { |
224 store()->RemoveLoginsCreatedBetween(base::Time(), next_day, | 247 store()->RemoveLoginsCreatedBetween(base::Time(), next_day, |
225 base::Closure()); | 248 base::Closure()); |
226 } else { | 249 } else { |
227 store()->RemoveLoginsSyncedBetween(base::Time(), next_day); | 250 store()->RemoveLoginsSyncedBetween(base::Time(), next_day); |
228 } | 251 } |
229 FinishAsyncProcessing(); | 252 FinishAsyncProcessing(); |
230 } | 253 } |
231 | 254 |
232 TEST_F(PasswordStoreProxyMacTest, FormLifeCycle) { | 255 // ----------- Tests ------------- |
| 256 |
| 257 TEST_P(PasswordStoreProxyMacTest, StartAndStop) { |
| 258 // PasswordStore::Shutdown() immediately follows PasswordStore::Init(). The |
| 259 // message loop isn't running in between. Anyway, PasswordStore should end up |
| 260 // in the right state. |
| 261 ClosePasswordStore(); |
| 262 |
| 263 int status = testing_prefs_.GetInteger( |
| 264 password_manager::prefs::kKeychainMigrationStatus); |
| 265 if (GetParam() == MigrationStatus::NOT_STARTED || |
| 266 GetParam() == MigrationStatus::FAILED_ONCE) { |
| 267 EXPECT_EQ(static_cast<int>(MigrationStatus::MIGRATED), status); |
| 268 } else { |
| 269 EXPECT_EQ(static_cast<int>(GetParam()), status); |
| 270 } |
| 271 } |
| 272 |
| 273 TEST_P(PasswordStoreProxyMacTest, FormLifeCycle) { |
233 PasswordForm password_form; | 274 PasswordForm password_form; |
234 password_form.origin = GURL("http://example.com"); | 275 password_form.origin = GURL("http://example.com"); |
235 password_form.username_value = base::ASCIIToUTF16("test1@gmail.com"); | 276 password_form.username_value = base::ASCIIToUTF16("test1@gmail.com"); |
236 password_form.password_value = base::ASCIIToUTF16("12345"); | 277 password_form.password_value = base::ASCIIToUTF16("12345"); |
237 password_form.signon_realm = "http://example.com/"; | 278 password_form.signon_realm = "http://example.com/"; |
238 | 279 |
239 AddForm(password_form); | 280 AddForm(password_form); |
240 password_form.password_value = base::ASCIIToUTF16("password"); | 281 password_form.password_value = base::ASCIIToUTF16("password"); |
241 UpdateForm(password_form); | 282 UpdateForm(password_form); |
242 RemoveForm(password_form); | 283 RemoveForm(password_form); |
243 } | 284 } |
244 | 285 |
245 TEST_F(PasswordStoreProxyMacTest, TestRemoveLoginsCreatedBetween) { | 286 TEST_P(PasswordStoreProxyMacTest, TestRemoveLoginsCreatedBetween) { |
246 CheckRemoveLoginsBetween(true); | 287 CheckRemoveLoginsBetween(true); |
247 } | 288 } |
248 | 289 |
249 TEST_F(PasswordStoreProxyMacTest, TestRemoveLoginsSyncedBetween) { | 290 TEST_P(PasswordStoreProxyMacTest, TestRemoveLoginsSyncedBetween) { |
250 CheckRemoveLoginsBetween(false); | 291 CheckRemoveLoginsBetween(false); |
251 } | 292 } |
252 | 293 |
253 TEST_F(PasswordStoreProxyMacTest, FillLogins) { | 294 TEST_P(PasswordStoreProxyMacTest, FillLogins) { |
254 PasswordForm password_form; | 295 PasswordForm password_form; |
255 password_form.origin = GURL("http://example.com"); | 296 password_form.origin = GURL("http://example.com"); |
256 password_form.signon_realm = "http://example.com/"; | 297 password_form.signon_realm = "http://example.com/"; |
257 password_form.username_value = base::ASCIIToUTF16("test1@gmail.com"); | 298 password_form.username_value = base::ASCIIToUTF16("test1@gmail.com"); |
258 password_form.password_value = base::ASCIIToUTF16("12345"); | 299 password_form.password_value = base::ASCIIToUTF16("12345"); |
259 AddForm(password_form); | 300 AddForm(password_form); |
260 | 301 |
261 PasswordForm blacklisted_form; | 302 PasswordForm blacklisted_form; |
262 blacklisted_form.origin = GURL("http://example2.com"); | 303 blacklisted_form.origin = GURL("http://example2.com"); |
263 blacklisted_form.signon_realm = "http://example2.com/"; | 304 blacklisted_form.signon_realm = "http://example2.com/"; |
(...skipping 14 matching lines...) Expand all Loading... |
278 .WillOnce(QuitUIMessageLoop()); | 319 .WillOnce(QuitUIMessageLoop()); |
279 base::MessageLoop::current()->Run(); | 320 base::MessageLoop::current()->Run(); |
280 | 321 |
281 store()->GetAutofillableLogins(&mock_consumer); | 322 store()->GetAutofillableLogins(&mock_consumer); |
282 EXPECT_CALL(mock_consumer, OnGetPasswordStoreResultsConstRef( | 323 EXPECT_CALL(mock_consumer, OnGetPasswordStoreResultsConstRef( |
283 ElementsAre(Pointee(password_form)))) | 324 ElementsAre(Pointee(password_form)))) |
284 .WillOnce(QuitUIMessageLoop()); | 325 .WillOnce(QuitUIMessageLoop()); |
285 base::MessageLoop::current()->Run(); | 326 base::MessageLoop::current()->Run(); |
286 } | 327 } |
287 | 328 |
288 TEST_F(PasswordStoreProxyMacTest, OperationsOnABadDatabaseSilentlyFail) { | 329 TEST_P(PasswordStoreProxyMacTest, OperationsOnABadDatabaseSilentlyFail) { |
289 // Verify that operations on a PasswordStore with a bad database cause no | 330 // Verify that operations on a PasswordStore with a bad database cause no |
290 // explosions, but fail without side effect, return no data and trigger no | 331 // explosions, but fail without side effect, return no data and trigger no |
291 // notifications. | 332 // notifications. |
292 ClosePasswordStore(); | 333 ClosePasswordStore(); |
293 CreateAndInitPasswordStore(make_scoped_ptr(new BadLoginDatabase)); | 334 CreateAndInitPasswordStore(make_scoped_ptr(new BadLoginDatabase)); |
294 FinishAsyncProcessing(); | 335 FinishAsyncProcessing(); |
295 EXPECT_FALSE(login_db()); | 336 EXPECT_FALSE(login_db()); |
296 | 337 |
297 // The store should outlive the observer. | 338 // The store should outlive the observer. |
298 scoped_refptr<PasswordStoreProxyMac> store_refptr = store(); | 339 scoped_refptr<PasswordStoreProxyMac> store_refptr = store(); |
(...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
344 // Delete one login; a range of logins. | 385 // Delete one login; a range of logins. |
345 store()->RemoveLogin(*form); | 386 store()->RemoveLogin(*form); |
346 store()->RemoveLoginsCreatedBetween(base::Time(), base::Time::Max(), | 387 store()->RemoveLoginsCreatedBetween(base::Time(), base::Time::Max(), |
347 base::Closure()); | 388 base::Closure()); |
348 store()->RemoveLoginsSyncedBetween(base::Time(), base::Time::Max()); | 389 store()->RemoveLoginsSyncedBetween(base::Time(), base::Time::Max()); |
349 FinishAsyncProcessing(); | 390 FinishAsyncProcessing(); |
350 | 391 |
351 // Verify no notifications are fired during shutdown either. | 392 // Verify no notifications are fired during shutdown either. |
352 ClosePasswordStore(); | 393 ClosePasswordStore(); |
353 } | 394 } |
| 395 |
| 396 INSTANTIATE_TEST_CASE_P(, |
| 397 PasswordStoreProxyMacTest, |
| 398 testing::Values(MigrationStatus::NOT_STARTED, |
| 399 MigrationStatus::MIGRATED, |
| 400 MigrationStatus::FAILED_ONCE, |
| 401 MigrationStatus::FAILED_TWICE)); |
| 402 |
| 403 // Test the migration process. |
| 404 class PasswordStoreProxyMacMigrationTest : public PasswordStoreProxyMacTest { |
| 405 public: |
| 406 void SetUp() override; |
| 407 |
| 408 void TestMigration(bool lock_keychain); |
| 409 |
| 410 protected: |
| 411 scoped_ptr<password_manager::LoginDatabase> login_db_; |
| 412 scoped_ptr<crypto::MockAppleKeychain> keychain_; |
| 413 }; |
| 414 |
| 415 void PasswordStoreProxyMacMigrationTest::SetUp() { |
| 416 login_db_.reset( |
| 417 new password_manager::LoginDatabase(test_login_db_file_path())); |
| 418 keychain_.reset(new crypto::MockAppleKeychain); |
| 419 } |
| 420 |
| 421 void PasswordStoreProxyMacMigrationTest::TestMigration(bool lock_keychain) { |
| 422 PasswordForm form; |
| 423 form.origin = GURL("http://accounts.google.com/LoginAuth"); |
| 424 form.signon_realm = "http://accounts.google.com/"; |
| 425 form.username_value = base::ASCIIToUTF16("my_username"); |
| 426 form.password_value = base::ASCIIToUTF16("12345"); |
| 427 |
| 428 if (GetParam() != MigrationStatus::MIGRATED) |
| 429 login_db_->set_clear_password_values(true); |
| 430 EXPECT_TRUE(login_db_->Init()); |
| 431 EXPECT_EQ(AddChangeForForm(form), login_db_->AddLogin(form)); |
| 432 // Prepare another database instance with the same content which is to be |
| 433 // initialized by PasswordStoreProxyMac. |
| 434 login_db_.reset( |
| 435 new password_manager::LoginDatabase(test_login_db_file_path())); |
| 436 MacKeychainPasswordFormAdapter adapter(keychain_.get()); |
| 437 EXPECT_TRUE(adapter.AddPassword(form)); |
| 438 |
| 439 // Init the store. It may trigger the migration. |
| 440 if (lock_keychain) |
| 441 keychain_->set_locked(true); |
| 442 crypto::MockAppleKeychain* weak_keychain = keychain_.get(); |
| 443 store_ = new PasswordStoreProxyMac( |
| 444 BrowserThread::GetMessageLoopProxyForThread(BrowserThread::UI), |
| 445 keychain_.Pass(), login_db_.Pass(), &testing_prefs_); |
| 446 ASSERT_TRUE(store_->Init(syncer::SyncableService::StartSyncFlare())); |
| 447 FinishAsyncProcessing(); |
| 448 |
| 449 // Check the password is still there. |
| 450 if (lock_keychain) |
| 451 weak_keychain->set_locked(false); |
| 452 MockPasswordStoreConsumer mock_consumer; |
| 453 store()->GetLogins(form, PasswordStoreProxyMac::ALLOW_PROMPT, &mock_consumer); |
| 454 EXPECT_CALL(mock_consumer, |
| 455 OnGetPasswordStoreResultsConstRef(ElementsAre(Pointee(form)))) |
| 456 .WillOnce(QuitUIMessageLoop()); |
| 457 base::MessageLoop::current()->Run(); |
| 458 |
| 459 int status = testing_prefs_.GetInteger( |
| 460 password_manager::prefs::kKeychainMigrationStatus); |
| 461 if (GetParam() == MigrationStatus::MIGRATED || |
| 462 GetParam() == MigrationStatus::FAILED_TWICE) { |
| 463 EXPECT_EQ(static_cast<int>(GetParam()), status); |
| 464 } else if (lock_keychain) { |
| 465 EXPECT_EQ(static_cast<int>(GetParam() == MigrationStatus::NOT_STARTED |
| 466 ? MigrationStatus::FAILED_ONCE |
| 467 : MigrationStatus::FAILED_TWICE), |
| 468 status); |
| 469 } else { |
| 470 EXPECT_EQ(static_cast<int>(MigrationStatus::MIGRATED), status); |
| 471 } |
| 472 } |
| 473 |
| 474 TEST_P(PasswordStoreProxyMacMigrationTest, TestSuccessfullMigration) { |
| 475 TestMigration(false); |
| 476 } |
| 477 |
| 478 TEST_P(PasswordStoreProxyMacMigrationTest, TestFailedMigration) { |
| 479 TestMigration(true); |
| 480 } |
| 481 |
| 482 INSTANTIATE_TEST_CASE_P(, |
| 483 PasswordStoreProxyMacMigrationTest, |
| 484 testing::Values(MigrationStatus::NOT_STARTED, |
| 485 MigrationStatus::MIGRATED, |
| 486 MigrationStatus::FAILED_ONCE, |
| 487 MigrationStatus::FAILED_TWICE)); |
| 488 |
354 } // namespace | 489 } // namespace |
OLD | NEW |