/* * Copyright 2017-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0/ * * or in the "license" file accompanying this file. This file is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ /// @file SettingsTest.cpp #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Settings/Settings.h" #include "Settings/SettingsUpdatedEventSender.h" #include "Settings/SQLiteSettingStorage.h" namespace alexaClientSDK { namespace capabilityAgents { namespace settings { namespace test { using namespace avsCommon::avs::initialization; using namespace avsCommon::sdkInterfaces; using namespace avsCommon::sdkInterfaces::test; using namespace avsCommon::utils::configuration; using namespace avsCommon::utils::json::jsonUtils; using namespace ::testing; using namespace certifiedSender; /// JSON key for the event section of a message. static const std::string MESSAGE_EVENT_KEY = "event"; /// JSON key for the header section of a message. static const std::string MESSAGE_HEADER_KEY = "header"; /// JSON key for the namespace field of a message header. static const std::string MESSAGE_NAMESPACE_KEY = "namespace"; /// JSON key for the name field of a message header. static const std::string MESSAGE_NAME_KEY = "name"; /// JSON key for the message ID field of a message header. static const std::string MESSAGE_ID_KEY = "messageId"; /// JSON key for the payload section of an message. static const std::string MESSAGE_PAYLOAD_KEY = "payload"; /// JSON key for the settings array filed of the message. static const std::string MESSAGE_SETTINGS_KEY = "settings"; /// The namespace for this event. static const std::string SETTINGS_NAMESPACE = "Settings"; /// JSON value for a SettingsUpdated event's name. static const std::string SETTINGS_UPDATED_EVENT_NAME = "SettingsUpdated"; /// JSON value for the settings array's key. static const std::string SETTINGS_KEY = "key"; /// JSON value for the settings array's value. static const std::string SETTINGS_VALUE = "value"; /// JSON text for settings config values for initialization of settings Object. // clang-format off static const std::string SETTINGS_CONFIG_JSON = "{" "\"certifiedSender\":{" "\"databaseFilePath\":\"database.db\"" "}," "\"settings\":{" "\"databaseFilePath\":\"settingsUnitTest.db\"," "\"defaultAVSClientSettings\":{" "\"locale\":\"en-GB\"" "}" "}" "}"; // clang-format on /** * Utility function to determine if the storage component is opened. * * @param storage The storage component to check. * @return True if the storage component's underlying database is opened, false otherwise. */ static bool isOpen(const std::shared_ptr& storage) { std::unordered_map dummyMapOfSettings; return storage->load(&dummyMapOfSettings); } /** * This class allows us to test SingleSettingObserver interaction. */ class MockSingleSettingObserver : public SingleSettingObserverInterface { public: MockSingleSettingObserver() { } MOCK_METHOD2(onSettingChanged, void(const std::string& key, const std::string& value)); }; /** * This class allows us to test GlobalSettingsObserver interaction. */ class MockGlobalSettingsObserver : public GlobalSettingsObserverInterface { public: MockGlobalSettingsObserver() { } MOCK_METHOD1(onSettingChanged, void(const std::unordered_map& mapOfSettings)); }; /** * Class with which to mock a connection ot AVS. */ class MockConnection : public avsCommon::avs::AbstractAVSConnectionManager { public: MockConnection() = default; MOCK_METHOD0(enable, void()); MOCK_METHOD0(disable, void()); MOCK_METHOD0(isEnabled, bool()); MOCK_METHOD0(reconnect, void()); MOCK_METHOD1( addMessageObserver, void(std::shared_ptr observer)); MOCK_METHOD1( removeMessageObserver, void(std::shared_ptr observer)); bool isConnected() const; /** * Update the connection status. * * @param status The Connection Status. * @param reason The reason the Connection Status changed. */ void updateConnectionStatus( ConnectionStatusObserverInterface::Status status, ConnectionStatusObserverInterface::ChangedReason reason); }; bool MockConnection::isConnected() const { return ConnectionStatusObserverInterface::Status::CONNECTED == m_connectionStatus; } void MockConnection::updateConnectionStatus( ConnectionStatusObserverInterface::Status status, ConnectionStatusObserverInterface::ChangedReason reason) { AbstractAVSConnectionManager::updateConnectionStatus(status, reason); } class MockMessageStorage : public MessageStorageInterface { public: MOCK_METHOD0(createDatabase, bool()); MOCK_METHOD0(open, bool()); MOCK_METHOD0(close, void()); MOCK_METHOD2(store, bool(const std::string& message, int* id)); MOCK_METHOD1(load, bool(std::queue* messageContainer)); MOCK_METHOD1(erase, bool(int messageId)); MOCK_METHOD0(clearDatabase, bool()); virtual ~MockMessageStorage() = default; }; /** * Utility class to take the parameters of SettingsUpdated event and provide functionalities * to verify the event being sent. */ class SettingsVerifyTest { public: /** * Constructs an object which captures pair of settings in the map. */ SettingsVerifyTest(std::unordered_map mapOfSettings); /** * This function verifies that the JSON content of SettingsUpdated event @c MessageRequest is correct. * This function signature matches that of @c MessageSenderInterface::sendMessage() so that an * @c ON_CALL() can @c Invoke() this function directly. * * @param request The @c MessageRequest to verify. */ void verifyMessage(std::shared_ptr request); private: /// The map of settings which holds the pair of settings sent to AVS. std::unordered_map m_mapOfSettings; }; SettingsVerifyTest::SettingsVerifyTest(std::unordered_map mapOfSettings) : m_mapOfSettings{mapOfSettings} { } void SettingsVerifyTest::verifyMessage(std::shared_ptr request) { rapidjson::Document document; document.Parse(request->getJsonContent().c_str()); EXPECT_FALSE(document.HasParseError()) << "rapidjson detected a parsing error at offset:" << std::to_string(document.GetErrorOffset()) << ", error message: " << GetParseError_En(document.GetParseError()); auto event = document.FindMember(MESSAGE_EVENT_KEY); ASSERT_NE(event, document.MemberEnd()); auto header = event->value.FindMember(MESSAGE_HEADER_KEY); ASSERT_NE(header, event->value.MemberEnd()); auto payload = event->value.FindMember(MESSAGE_PAYLOAD_KEY); ASSERT_NE(payload, event->value.MemberEnd()); auto settings = payload->value.FindMember(MESSAGE_SETTINGS_KEY); ASSERT_NE(settings, payload->value.MemberEnd()); std::string temp_string = ""; ASSERT_TRUE(retrieveValue(header->value, MESSAGE_NAMESPACE_KEY, &temp_string)); EXPECT_EQ(temp_string, SETTINGS_NAMESPACE); ASSERT_TRUE(retrieveValue(header->value, MESSAGE_NAME_KEY, &temp_string)); EXPECT_EQ(temp_string, SETTINGS_UPDATED_EVENT_NAME); ASSERT_TRUE(retrieveValue(header->value, MESSAGE_ID_KEY, &temp_string)); ASSERT_NE(temp_string, ""); ASSERT_TRUE(jsonArrayExists(payload->value, MESSAGE_SETTINGS_KEY)); rapidjson::Value& array = settings->value; ASSERT_TRUE(array.Size() == m_mapOfSettings.size()); rapidjson::SizeType i; std::unordered_map::const_iterator j; for (i = array.Size() - 1, j = m_mapOfSettings.begin(); j != m_mapOfSettings.end(); i--, j++) { auto key = array[i].FindMember(SETTINGS_KEY); ASSERT_NE(key, array[i].MemberEnd()); auto value = array[i].FindMember(SETTINGS_VALUE); ASSERT_NE(value, array[i].MemberEnd()); ASSERT_TRUE(retrieveValue(array[i], SETTINGS_KEY, &temp_string)); EXPECT_EQ(temp_string, j->first); ASSERT_TRUE(retrieveValue(array[i], SETTINGS_VALUE, &temp_string)); EXPECT_EQ(temp_string, j->second); } } /// Test harness for @c Settings class. class SettingsTest : public ::testing::Test { public: void SetUp() override; void TearDown() override; protected: /** * Function to send a SettingsUpdated event and verify that it succeeds. Parameters are passed through * @c SettingsVerifyTest::SettingsVerifyTest(). * * @param key The name of the setting to change. * @param value The value of the setting to which it has to be set. * @return @c true if the SettingsUpdated event was sent correctly, else @c false. */ bool testChangeSettingSucceeds(const std::string& key, const std::string& value); /// The mock @c MessageSenderInterface. std::shared_ptr m_mockMessageSender; /// Global Observer for all the settings which sends the event. std::shared_ptr m_settingsEventSender; /// Settings Storage object. std::shared_ptr m_storage; /// The @c Settings Object to test. std::shared_ptr m_settingsObject; /// The object which verifies the json of message sent. std::shared_ptr m_settingsVerifyObject; /// The map which stores all the settings for the object. std::unordered_map m_mapOfSettings; /// The data manager required to build the base object std::shared_ptr m_dataManager; /// Class under test. std::shared_ptr m_certifiedSender; /// Mock message storage layer. std::shared_ptr m_msgStorage; /// Mocked connection std::shared_ptr> m_mockConnection; }; void SettingsTest::SetUp() { m_dataManager = std::make_shared(); auto inString = std::shared_ptr(new std::istringstream(SETTINGS_CONFIG_JSON)); ASSERT_TRUE(AlexaClientSDKInit::initialize({inString})); m_mockMessageSender = std::make_shared(); m_mockConnection = std::make_shared>(); m_msgStorage = std::make_shared(); EXPECT_CALL(*m_msgStorage, open()).WillOnce(Return(true)); m_certifiedSender = CertifiedSender::create(m_mockMessageSender, m_mockConnection, m_msgStorage, m_dataManager); m_mockConnection->updateConnectionStatus( ConnectionStatusObserverInterface::Status::CONNECTED, ConnectionStatusObserverInterface::ChangedReason::ACL_CLIENT_REQUEST); m_settingsEventSender = SettingsUpdatedEventSender::create(m_certifiedSender); ASSERT_NE(m_settingsEventSender, nullptr); m_storage = std::make_shared("settingsUnitTest.db"); m_settingsObject = Settings::create(m_storage, {m_settingsEventSender}, m_dataManager); ASSERT_NE(m_settingsObject, nullptr); ASSERT_TRUE(m_storage->load(&m_mapOfSettings)); } void SettingsTest::TearDown() { m_mapOfSettings.clear(); m_storage->clearDatabase(); AlexaClientSDKInit::uninitialize(); m_mockConnection->removeConnectionStatusObserver(m_certifiedSender); m_certifiedSender->shutdown(); } bool SettingsTest::testChangeSettingSucceeds(const std::string& key, const std::string& value) { std::mutex mutex; std::condition_variable conditionVariable; bool done = false; EXPECT_CALL(*m_msgStorage, store(_, _)).WillRepeatedly(Return(true)); EXPECT_CALL(*m_mockMessageSender, sendMessage(_)) .WillRepeatedly(DoAll( Invoke([this, key, value](std::shared_ptr request) { m_mapOfSettings[key] = value; m_settingsVerifyObject = std::make_shared(m_mapOfSettings); m_settingsVerifyObject->verifyMessage(request); }), InvokeWithoutArgs([&] { std::lock_guard lock(mutex); done = true; conditionVariable.notify_one(); }))); m_settingsObject->changeSetting(key, value); std::unique_lock lock(mutex); return conditionVariable.wait_for(lock, std::chrono::seconds(1), [&done] { return done; }); } /** * Test to verify the @c create function of @c Settings class. */ TEST_F(SettingsTest, createTest) { ASSERT_EQ( nullptr, m_settingsObject->create( m_storage, std::unordered_set>(), m_dataManager)); ASSERT_EQ(nullptr, m_settingsObject->create(nullptr, {m_settingsEventSender}, m_dataManager)); ASSERT_EQ(nullptr, m_settingsObject->create(m_storage, {nullptr}, m_dataManager)); } /** * Test to verify if by adding a global observer and changing the setting, * the global observer is notified of the change. It also verifies that event is being sent in correct JSON format. */ TEST_F(SettingsTest, addGlobalSettingsObserverTest) { std::shared_ptr mockGlobalSettingObserver; mockGlobalSettingObserver = std::make_shared(); m_settingsObject->addGlobalSettingsObserver(mockGlobalSettingObserver); EXPECT_CALL(*mockGlobalSettingObserver, onSettingChanged(_)).Times(1); ASSERT_TRUE(testChangeSettingSucceeds("locale", "en-US")); } /** * Test to verify if by removing a global observer and changing the setting, * the global observer is not notified of the change. It also verifies that event is being sent in correct JSON format. */ TEST_F(SettingsTest, removeGlobalSettingsObserverTest) { std::shared_ptr mockGlobalSettingObserver; mockGlobalSettingObserver = std::make_shared(); m_settingsObject->removeGlobalSettingsObserver(mockGlobalSettingObserver); EXPECT_CALL(*mockGlobalSettingObserver, onSettingChanged(_)).Times(0); ASSERT_TRUE(testChangeSettingSucceeds("locale", "en-US")); } /** * Test to verify @c addSingleSettingObserver function. This test verifies if the setting key is invalid * i.e. it doesn't exist in the list of @c SETTINGS_ACCEPTED_KEYS, the observer corresponding to it * will not be added. */ TEST_F(SettingsTest, addSingleSettingObserverWithInvalidKeyTest) { std::shared_ptr wakewordObserver; wakewordObserver = std::make_shared(); m_settingsObject->addSingleSettingObserver("wakeword", wakewordObserver); EXPECT_CALL(*wakewordObserver, onSettingChanged(_, _)).Times(0); ASSERT_FALSE(testChangeSettingSucceeds("wakeword", "Alexa")); std::shared_ptr localeObserver; localeObserver = std::make_shared(); m_settingsObject->addSingleSettingObserver("local", localeObserver); EXPECT_CALL(*localeObserver, onSettingChanged(_, _)).Times(0); ASSERT_FALSE(testChangeSettingSucceeds("local", "en-US")); } /** * Test to verify the function @c removeSingleSettingObserver. The test checks that if an observer is added for * an invalid key even if it is a typo, the observer will not be removed. * It also verifies that event is being sent in correct JSON format. */ TEST_F(SettingsTest, removeSingleSettingObserverWithInvalidKeyTest) { std::shared_ptr localeObserver; localeObserver = std::make_shared(); m_settingsObject->addSingleSettingObserver("locale", localeObserver); m_settingsObject->removeSingleSettingObserver("local", localeObserver); EXPECT_CALL(*localeObserver, onSettingChanged(_, _)).Times(1); ASSERT_TRUE(testChangeSettingSucceeds("locale", "en-US")); } /** * Test verifies that if an observer is removed with a valid key, the observer gets removed * and is not notified when the setting changes. It also verifies that event is being sent in correct JSON format. */ TEST_F(SettingsTest, removeSingleSettingObserverWithCorrectKeyTest) { std::shared_ptr localeObserver; localeObserver = std::make_shared(); m_settingsObject->addSingleSettingObserver("locale", localeObserver); m_settingsObject->removeSingleSettingObserver("locale", localeObserver); EXPECT_CALL(*localeObserver, onSettingChanged(_, _)).Times(0); ASSERT_TRUE(testChangeSettingSucceeds("locale", "en-US")); } /** * Test to check if the settings loaded from the database are same as the default settings. */ TEST_F(SettingsTest, defaultSettingsCorrect) { std::string DEFAULT_SETTINGS = "defaultAVSClientSettings"; std::string settings_json = ""; retrieveValue(SETTINGS_CONFIG_JSON, MESSAGE_SETTINGS_KEY, &settings_json); std::string default_settings = ""; retrieveValue(settings_json, DEFAULT_SETTINGS, &default_settings); rapidjson::Document document; ASSERT_TRUE(parseJSON(default_settings, &document)); std::string value = ""; for (auto& it : m_mapOfSettings) { auto key = document.FindMember(it.first); ASSERT_NE(key, document.MemberEnd()); ASSERT_TRUE(retrieveValue(document, it.first, &value)); EXPECT_EQ(it.second, value); } } /** * Test to check that @c clearData() removes any setting stored in the database. */ TEST_F(SettingsTest, clearDataTest) { ASSERT_TRUE(testChangeSettingSucceeds("locale", "en-CA")); m_settingsObject->clearData(); std::unordered_map tempMap; ASSERT_TRUE(m_storage->load(&tempMap)); ASSERT_TRUE(tempMap.empty()); } /** * Test to check clear database works as expected. */ TEST_F(SettingsTest, clearDatabaseTest) { std::unordered_map tempMap; ASSERT_TRUE(m_storage->clearDatabase()); ASSERT_TRUE(m_storage->load(&tempMap)); ASSERT_TRUE(tempMap.empty()); } /** * Test to check the store function of SQLiteSettingStorage class. */ TEST_F(SettingsTest, storeDatabaseTest) { ASSERT_TRUE(m_storage->clearDatabase()); std::map MapToStore = {{"wakeword", "Alexa"}, {"locale", "en-US"}}; for (auto& it : MapToStore) { ASSERT_TRUE(m_storage->store(it.first, it.second)); } std::unordered_map LoadMap; ASSERT_TRUE(m_storage->load(&LoadMap)); ASSERT_EQ(LoadMap.size(), MapToStore.size()); ASSERT_TRUE(std::equal(MapToStore.rbegin(), MapToStore.rend(), LoadMap.begin())); } /** * Test to check the modify function of SQLiteSettingStorage class. */ TEST_F(SettingsTest, modifyDatabaseTest) { ASSERT_TRUE(m_storage->modify("locale", "en-US")); ASSERT_FALSE(m_storage->modify("local", "en-GB")); ASSERT_TRUE(m_storage->clearDatabase()); ASSERT_FALSE(m_storage->settingExists("locale")); } /** * Test to check the erase function of SQLiteSettingStorage class. */ TEST_F(SettingsTest, eraseTest) { ASSERT_TRUE(m_storage->erase("locale")); ASSERT_FALSE(m_storage->settingExists("locale")); ASSERT_FALSE(m_storage->erase("local")); } /** * Test to check the createDatabase function of SQLiteSettingStorage class. */ TEST_F(SettingsTest, createDatabaseTest) { m_storage->close(); ASSERT_FALSE(m_storage->createDatabase()); } /** * Test to check the open and close functions of SQLiteSettingStorage class. */ TEST_F(SettingsTest, openAndCloseDatabaseTest) { ASSERT_FALSE(m_storage->open()); ASSERT_TRUE(isOpen(m_storage)); m_storage->close(); ASSERT_TRUE(m_storage->open()); ASSERT_TRUE(isOpen(m_storage)); } } // namespace test } // namespace settings } // namespace capabilityAgents } // namespace alexaClientSDK int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); if (argc < 1) { std::cerr << "USAGE: " << std::string(argv[0]) << std::endl; return 1; } return RUN_ALL_TESTS(); }