// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "content/browser/notifications/notification_database.h" #include "base/files/scoped_temp_dir.h" #include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" #include "content/public/browser/notification_database_data.h" #include "content/public/common/platform_notification_data.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/leveldatabase/src/include/leveldb/db.h" #include "third_party/leveldatabase/src/include/leveldb/write_batch.h" #include "url/gurl.h" namespace content { const int kExampleServiceWorkerRegistrationId = 42; const struct { const char* origin; int64_t service_worker_registration_id; } kExampleNotificationData[] = { { "https://example.com", 0 }, { "https://example.com", kExampleServiceWorkerRegistrationId }, { "https://example.com", kExampleServiceWorkerRegistrationId }, { "https://example.com", kExampleServiceWorkerRegistrationId + 1 }, { "https://chrome.com", 0 }, { "https://chrome.com", 0 }, { "https://chrome.com", kExampleServiceWorkerRegistrationId } }; class NotificationDatabaseTest : public ::testing::Test { protected: // Creates a new NotificationDatabase instance in memory. NotificationDatabase* CreateDatabaseInMemory() { return new NotificationDatabase(base::FilePath()); } // Creates a new NotificationDatabase instance in |path|. NotificationDatabase* CreateDatabaseOnFileSystem( const base::FilePath& path) { return new NotificationDatabase(path); } // Creates a new notification for |service_worker_registration_id| belonging // to |origin| and writes it to the database. The written notification id // will be stored in |notification_id|. void CreateAndWriteNotification(NotificationDatabase* database, const GURL& origin, int64_t service_worker_registration_id, int64_t* notification_id) { NotificationDatabaseData database_data; database_data.origin = origin; database_data.service_worker_registration_id = service_worker_registration_id; ASSERT_EQ(NotificationDatabase::STATUS_OK, database->WriteNotificationData(origin, database_data, notification_id)); } // Populates |database| with a series of example notifications that differ in // their origin and Service Worker registration id. void PopulateDatabaseWithExampleData(NotificationDatabase* database) { int64_t notification_id; for (size_t i = 0; i < arraysize(kExampleNotificationData); ++i) { ASSERT_NO_FATAL_FAILURE(CreateAndWriteNotification( database, GURL(kExampleNotificationData[i].origin), kExampleNotificationData[i].service_worker_registration_id, ¬ification_id)); } } // Returns if |database| has been opened. bool IsDatabaseOpen(NotificationDatabase* database) { return database->IsOpen(); } // Returns if |database| is an in-memory only database. bool IsInMemoryDatabase(NotificationDatabase* database) { return database->IsInMemoryDatabase(); } // Writes a LevelDB key-value pair directly to the LevelDB backing the // notification database in |database|. void WriteLevelDBKeyValuePair(NotificationDatabase* database, const std::string& key, const std::string& value) { leveldb::Status status = database->GetDBForTesting()->Put(leveldb::WriteOptions(), key, value); ASSERT_TRUE(status.ok()); } }; TEST_F(NotificationDatabaseTest, OpenCloseMemory) { scoped_ptr database(CreateDatabaseInMemory()); // Should return false because the database does not exist in memory. EXPECT_EQ(NotificationDatabase::STATUS_ERROR_NOT_FOUND, database->Open(false /* create_if_missing */)); // Should return true, indicating that the database could be created. EXPECT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); EXPECT_TRUE(IsDatabaseOpen(database.get())); EXPECT_TRUE(IsInMemoryDatabase(database.get())); // Verify that in-memory databases do not persist when being re-created. database.reset(CreateDatabaseInMemory()); EXPECT_EQ(NotificationDatabase::STATUS_ERROR_NOT_FOUND, database->Open(false /* create_if_missing */)); } TEST_F(NotificationDatabaseTest, OpenCloseFileSystem) { base::ScopedTempDir database_dir; ASSERT_TRUE(database_dir.CreateUniqueTempDir()); scoped_ptr database( CreateDatabaseOnFileSystem(database_dir.path())); // Should return false because the database does not exist on the file system. EXPECT_EQ(NotificationDatabase::STATUS_ERROR_NOT_FOUND, database->Open(false /* create_if_missing */)); // Should return true, indicating that the database could be created. EXPECT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); EXPECT_TRUE(IsDatabaseOpen(database.get())); EXPECT_FALSE(IsInMemoryDatabase(database.get())); // Close the database, and re-open it without attempting to create it because // the files on the file system should still exist as expected. database.reset(CreateDatabaseOnFileSystem(database_dir.path())); EXPECT_EQ(NotificationDatabase::STATUS_OK, database->Open(false /* create_if_missing */)); } TEST_F(NotificationDatabaseTest, DestroyDatabase) { base::ScopedTempDir database_dir; ASSERT_TRUE(database_dir.CreateUniqueTempDir()); scoped_ptr database( CreateDatabaseOnFileSystem(database_dir.path())); EXPECT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); EXPECT_TRUE(IsDatabaseOpen(database.get())); // Destroy the database. This will immediately close it as well. ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Destroy()); EXPECT_FALSE(IsDatabaseOpen(database.get())); // Try to re-open the database (but not re-create it). This should fail as // the files associated with the database should have been blown away. database.reset(CreateDatabaseOnFileSystem(database_dir.path())); EXPECT_EQ(NotificationDatabase::STATUS_ERROR_NOT_FOUND, database->Open(false /* create_if_missing */)); } TEST_F(NotificationDatabaseTest, NotificationIdIncrements) { base::ScopedTempDir database_dir; ASSERT_TRUE(database_dir.CreateUniqueTempDir()); scoped_ptr database( CreateDatabaseOnFileSystem(database_dir.path())); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); GURL origin("https://example.com"); int64_t notification_id = 0; // Verify that getting two ids on the same database instance results in // incrementing values. Notification ids will start at 1. ASSERT_NO_FATAL_FAILURE(CreateAndWriteNotification( database.get(), origin, 0 /* sw_registration_id */, ¬ification_id)); EXPECT_EQ(notification_id, 1); ASSERT_NO_FATAL_FAILURE(CreateAndWriteNotification( database.get(), origin, 0 /* sw_registration_id */, ¬ification_id)); EXPECT_EQ(notification_id, 2); database.reset(CreateDatabaseOnFileSystem(database_dir.path())); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(false /* create_if_missing */)); // Verify that the next notification id was stored in the database, and // continues where we expect it to be, even after closing and opening it. ASSERT_NO_FATAL_FAILURE(CreateAndWriteNotification( database.get(), origin, 0 /* sw_registration_id */, ¬ification_id)); EXPECT_EQ(notification_id, 3); } TEST_F(NotificationDatabaseTest, NotificationIdIncrementsStorage) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); GURL origin("https://example.com"); NotificationDatabaseData database_data; database_data.notification_id = -1; int64_t notification_id = 0; ASSERT_EQ(NotificationDatabase::STATUS_OK, database->WriteNotificationData(origin, database_data, ¬ification_id)); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->ReadNotificationData(notification_id, origin, &database_data)); EXPECT_EQ(notification_id, database_data.notification_id); } TEST_F(NotificationDatabaseTest, NotificationIdCorruption) { base::ScopedTempDir database_dir; ASSERT_TRUE(database_dir.CreateUniqueTempDir()); scoped_ptr database( CreateDatabaseOnFileSystem(database_dir.path())); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); GURL origin("https://example.com"); NotificationDatabaseData database_data; int64_t notification_id = 0; ASSERT_EQ(NotificationDatabase::STATUS_OK, database->WriteNotificationData(origin, database_data, ¬ification_id)); EXPECT_EQ(notification_id, 1); // Deliberately write an invalid value as the next notification id. When // re-opening the database, the Open() method should realize that an invalid // value is being read, and mark the database as corrupted. ASSERT_NO_FATAL_FAILURE(WriteLevelDBKeyValuePair(database.get(), "NEXT_NOTIFICATION_ID", "-42")); database.reset(CreateDatabaseOnFileSystem(database_dir.path())); EXPECT_EQ(NotificationDatabase::STATUS_ERROR_CORRUPTED, database->Open(false /* create_if_missing */)); } TEST_F(NotificationDatabaseTest, ReadInvalidNotificationData) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); NotificationDatabaseData database_data; // Reading the notification data for a notification that does not exist should // return the ERROR_NOT_FOUND status code. EXPECT_EQ(NotificationDatabase::STATUS_ERROR_NOT_FOUND, database->ReadNotificationData(9001, GURL("https://chrome.com"), &database_data)); } TEST_F(NotificationDatabaseTest, ReadNotificationDataDifferentOrigin) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); int64_t notification_id = 0; GURL origin("https://example.com"); NotificationDatabaseData database_data, read_database_data; database_data.notification_data.title = base::UTF8ToUTF16("My Notification"); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->WriteNotificationData(origin, database_data, ¬ification_id)); // Reading the notification from the database when given a different origin // should return the ERROR_NOT_FOUND status code. EXPECT_EQ(NotificationDatabase::STATUS_ERROR_NOT_FOUND, database->ReadNotificationData(notification_id, GURL("https://chrome.com"), &read_database_data)); // However, reading the notification from the database with the same origin // should return STATUS_OK and the associated notification data. ASSERT_EQ(NotificationDatabase::STATUS_OK, database->ReadNotificationData(notification_id, origin, &read_database_data)); EXPECT_EQ(database_data.notification_data.title, read_database_data.notification_data.title); } TEST_F(NotificationDatabaseTest, ReadNotificationDataReflection) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); int64_t notification_id = 0; GURL origin("https://example.com"); PlatformNotificationData notification_data; notification_data.title = base::UTF8ToUTF16("My Notification"); notification_data.direction = PlatformNotificationData::DIRECTION_RIGHT_TO_LEFT; notification_data.lang = "nl-NL"; notification_data.body = base::UTF8ToUTF16("Hello, world!"); notification_data.tag = "replace id"; notification_data.icon = GURL("https://example.com/icon.png"); notification_data.silent = true; NotificationDatabaseData database_data; database_data.notification_id = notification_id; database_data.origin = origin; database_data.service_worker_registration_id = 42; database_data.notification_data = notification_data; // Write the constructed notification to the database, and then immediately // read it back from the database again as well. ASSERT_EQ(NotificationDatabase::STATUS_OK, database->WriteNotificationData(origin, database_data, ¬ification_id)); NotificationDatabaseData read_database_data; ASSERT_EQ(NotificationDatabase::STATUS_OK, database->ReadNotificationData(notification_id, origin, &read_database_data)); // Verify that all members retrieved from the database are exactly the same // as the ones that were written to it. This tests the serialization behavior. EXPECT_EQ(notification_id, read_database_data.notification_id); EXPECT_EQ(database_data.origin, read_database_data.origin); EXPECT_EQ(database_data.service_worker_registration_id, read_database_data.service_worker_registration_id); const PlatformNotificationData& read_notification_data = read_database_data.notification_data; EXPECT_EQ(notification_data.title, read_notification_data.title); EXPECT_EQ(notification_data.direction, read_notification_data.direction); EXPECT_EQ(notification_data.lang, read_notification_data.lang); EXPECT_EQ(notification_data.body, read_notification_data.body); EXPECT_EQ(notification_data.tag, read_notification_data.tag); EXPECT_EQ(notification_data.icon, read_notification_data.icon); EXPECT_EQ(notification_data.silent, read_notification_data.silent); } TEST_F(NotificationDatabaseTest, ReadWriteMultipleNotificationData) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); GURL origin("https://example.com"); int64_t notification_id = 0; // Write ten notifications to the database, each with a unique title and // notification id (it is the responsibility of the user to increment this). for (int i = 1; i <= 10; ++i) { ASSERT_NO_FATAL_FAILURE(CreateAndWriteNotification( database.get(), origin, i /* sw_registration_id */, ¬ification_id)); EXPECT_EQ(notification_id, i); } NotificationDatabaseData database_data; // Read the ten notifications from the database, and verify that the titles // of each of them matches with how they were created. for (int i = 1; i <= 10; ++i) { ASSERT_EQ(NotificationDatabase::STATUS_OK, database->ReadNotificationData(i /* notification_id */, origin, &database_data)); EXPECT_EQ(i, database_data.service_worker_registration_id); } } TEST_F(NotificationDatabaseTest, DeleteInvalidNotificationData) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); // Deleting non-existing notifications is not considered to be a failure. ASSERT_EQ(NotificationDatabase::STATUS_OK, database->DeleteNotificationData(9001, GURL("https://chrome.com"))); } TEST_F(NotificationDatabaseTest, DeleteNotificationDataSameOrigin) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); int64_t notification_id = 0; NotificationDatabaseData database_data; GURL origin("https://example.com"); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->WriteNotificationData(origin, database_data, ¬ification_id)); // Reading a notification after writing one should succeed. EXPECT_EQ(NotificationDatabase::STATUS_OK, database->ReadNotificationData(notification_id, origin, &database_data)); // Delete the notification which was just written to the database, and verify // that reading it again will fail. EXPECT_EQ(NotificationDatabase::STATUS_OK, database->DeleteNotificationData(notification_id, origin)); EXPECT_EQ(NotificationDatabase::STATUS_ERROR_NOT_FOUND, database->ReadNotificationData(notification_id, origin, &database_data)); } TEST_F(NotificationDatabaseTest, DeleteNotificationDataDifferentOrigin) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); int64_t notification_id = 0; NotificationDatabaseData database_data; GURL origin("https://example.com"); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->WriteNotificationData(origin, database_data, ¬ification_id)); // Attempting to delete the notification with a different origin, but with the // same |notification_id|, should not return an error (the notification could // not be found, but that's not considered a failure). However, it should not // remove the notification either. EXPECT_EQ(NotificationDatabase::STATUS_OK, database->DeleteNotificationData(notification_id, GURL("https://chrome.com"))); EXPECT_EQ(NotificationDatabase::STATUS_OK, database->ReadNotificationData(notification_id, origin, &database_data)); } TEST_F(NotificationDatabaseTest, ReadAllNotificationData) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); ASSERT_NO_FATAL_FAILURE(PopulateDatabaseWithExampleData(database.get())); std::vector notifications; ASSERT_EQ(NotificationDatabase::STATUS_OK, database->ReadAllNotificationData(¬ifications)); EXPECT_EQ(arraysize(kExampleNotificationData), notifications.size()); } TEST_F(NotificationDatabaseTest, ReadAllNotificationDataEmpty) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); std::vector notifications; ASSERT_EQ(NotificationDatabase::STATUS_OK, database->ReadAllNotificationData(¬ifications)); EXPECT_EQ(0u, notifications.size()); } TEST_F(NotificationDatabaseTest, ReadAllNotificationDataForOrigin) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); ASSERT_NO_FATAL_FAILURE(PopulateDatabaseWithExampleData(database.get())); GURL origin("https://example.com"); std::vector notifications; ASSERT_EQ(NotificationDatabase::STATUS_OK, database->ReadAllNotificationDataForOrigin(origin, ¬ifications)); EXPECT_EQ(4u, notifications.size()); } TEST_F(NotificationDatabaseTest, ReadAllNotificationDataForServiceWorkerRegistration) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); ASSERT_NO_FATAL_FAILURE(PopulateDatabaseWithExampleData(database.get())); GURL origin("https://example.com:443"); std::vector notifications; ASSERT_EQ(NotificationDatabase::STATUS_OK, database->ReadAllNotificationDataForServiceWorkerRegistration( origin, kExampleServiceWorkerRegistrationId, ¬ifications)); EXPECT_EQ(2u, notifications.size()); } TEST_F(NotificationDatabaseTest, DeleteAllNotificationDataForOrigin) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); ASSERT_NO_FATAL_FAILURE(PopulateDatabaseWithExampleData(database.get())); GURL origin("https://example.com:443"); std::set deleted_notification_set; ASSERT_EQ(NotificationDatabase::STATUS_OK, database->DeleteAllNotificationDataForOrigin( origin, &deleted_notification_set)); EXPECT_EQ(4u, deleted_notification_set.size()); std::vector notifications; ASSERT_EQ(NotificationDatabase::STATUS_OK, database->ReadAllNotificationDataForOrigin(origin, ¬ifications)); EXPECT_EQ(0u, notifications.size()); } TEST_F(NotificationDatabaseTest, DeleteAllNotificationDataForOriginEmpty) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); GURL origin("https://example.com"); std::set deleted_notification_set; ASSERT_EQ(NotificationDatabase::STATUS_OK, database->DeleteAllNotificationDataForOrigin( origin, &deleted_notification_set)); EXPECT_EQ(0u, deleted_notification_set.size()); } TEST_F(NotificationDatabaseTest, DeleteAllNotificationDataForServiceWorkerRegistration) { scoped_ptr database(CreateDatabaseInMemory()); ASSERT_EQ(NotificationDatabase::STATUS_OK, database->Open(true /* create_if_missing */)); ASSERT_NO_FATAL_FAILURE(PopulateDatabaseWithExampleData(database.get())); GURL origin("https://example.com:443"); std::set deleted_notification_set; ASSERT_EQ(NotificationDatabase::STATUS_OK, database->DeleteAllNotificationDataForServiceWorkerRegistration( origin, kExampleServiceWorkerRegistrationId, &deleted_notification_set)); EXPECT_EQ(2u, deleted_notification_set.size()); std::vector notifications; ASSERT_EQ(NotificationDatabase::STATUS_OK, database->ReadAllNotificationDataForServiceWorkerRegistration( origin, kExampleServiceWorkerRegistrationId, ¬ifications)); EXPECT_EQ(0u, notifications.size()); } } // namespace content