// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #include "pch.h" #include #include "CalcViewModel\DataLoaders\CurrencyDataLoader.h" #include "CalcViewModel\Common\LocalizationService.h" using namespace CalculatorApp::Common; using namespace CalculatorApp::Common::LocalizationServiceProperties; using namespace CalculatorApp::DataLoaders; using namespace CalculatorApp::ViewModel; using namespace CalculatorUnitTests; using namespace Concurrency; using namespace Platform; using namespace std; using namespace UnitConversionManager; using namespace Windows::Foundation; using namespace Windows::Storage; using namespace Windows::Web::Http; namespace CalculatorApp { namespace DataLoaders { class MockCurrencyHttpClientWithResult : public CurrencyHttpClient { public: MockCurrencyHttpClientWithResult(String^ staticResponse, String^ allRatiosResponse) : m_staticResponse(staticResponse), m_allRatiosResponse(allRatiosResponse) { } IAsyncOperationWithProgress^ GetCurrencyMetadata() override { return ref new MockAsyncOperationWithProgress(m_staticResponse); } IAsyncOperationWithProgress^ GetCurrencyRatios() override { return ref new MockAsyncOperationWithProgress(m_allRatiosResponse); } private: String^ m_staticResponse; String^ m_allRatiosResponse; }; class MockCurrencyHttpClientThrowsException : public CurrencyHttpClient { public: MockCurrencyHttpClientThrowsException() {} IAsyncOperationWithProgress^ GetCurrencyMetadata() override { throw ref new NotImplementedException(); } IAsyncOperationWithProgress^ GetCurrencyRatios() override { throw ref new NotImplementedException(); } }; } } class DataLoadedCallback : public UnitConversionManager::IViewModelCurrencyCallback { public: DataLoadedCallback(task_completion_event tce) : m_task_completion_event{ tce } {} void CurrencyDataLoadFinished(bool didLoad) override { m_task_completion_event.set(); } void CurrencySymbolsCallback(_In_ const wstring& fromSymbol, _In_ const wstring& toSymbol) override {} void CurrencyRatiosCallback(_In_ const wstring& ratioEquality, _In_ const wstring& accRatioEquality) override {} void CurrencyTimestampCallback(_In_ const std::wstring& timestamp, bool isWeekOldData) override {} void NetworkBehaviorChanged(_In_ int newBehavior) override {} private: Concurrency::task_completion_event m_task_completion_event; }; namespace CalculatorUnitTests { constexpr auto sc_Language_EN = L"en-US"; const UCM::Category CURRENCY_CATEGORY = { NavCategory::Serialize(ViewMode::Currency), L"Currency", false /*supportsNegative*/ }; unique_ptr MakeLoaderWithResults(String^ staticResponse, String^ allRatiosResponse) { auto client = make_unique(staticResponse, allRatiosResponse); client->SetSourceCurrencyCode(StringReference(DefaultCurrencyCode.data())); return make_unique(move(client)); } String^ SerializeContent(const vector& data) { String^ result = L""; String^ delimiter = CurrencyDataLoaderConstants::CacheDelimiter; for (String^ content : data) { result += (delimiter + content); } return result; } bool WriteToFileInLocalCacheFolder(String^ filename, String^ content) { try { StorageFolder^ localFolder = ApplicationData::Current->LocalCacheFolder; StorageFile^ file = create_task(localFolder->CreateFileAsync(filename, CreationCollisionOption::ReplaceExisting)).get(); create_task(FileIO::WriteTextAsync(file, content)).wait(); return true; } catch (Exception^ ex) { return false; } } bool DeleteFileFromLocalCacheFolder(String^ filename) { try { StorageFolder^ folder = ApplicationData::Current->LocalCacheFolder; IAsyncOperation^ fileOperation = folder->GetFileAsync(filename); StorageFile^ file = create_task(fileOperation).get(); create_task(file->DeleteAsync()).get(); return true; } catch (Platform::Exception^ ex) { // FileNotFoundException is a valid result return ex->HResult == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); } catch (...) { return false; } } bool DeleteCurrencyCacheFiles() { try { bool deletedStaticData = DeleteFileFromLocalCacheFolder(CurrencyDataLoaderConstants::StaticDataFilename); bool deletedAllRatiosData = DeleteFileFromLocalCacheFolder(CurrencyDataLoaderConstants::AllRatiosDataFilename); return deletedStaticData && deletedAllRatiosData; } catch (...) { return false; } } void InsertToLocalSettings(String^ key, Object^ value) { ApplicationData::Current->LocalSettings->Values->Insert(key, value); } void RemoveFromLocalSettings(String^ key) { // Safe to call, even if the key does not exist. ApplicationData::Current->LocalSettings->Values->Remove(key); } void StandardCacheSetup() { // Insert current time so data is less than a day old. DateTime now = Utils::GetUniversalSystemTime(); InsertToLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey, now); InsertToLocalSettings(CurrencyDataLoaderConstants::CacheLangcodeKey, StringReference(sc_Language_EN)); VERIFY_IS_TRUE(DeleteCurrencyCacheFiles()); VERIFY_IS_TRUE(WriteToFileInLocalCacheFolder(CurrencyDataLoaderConstants::StaticDataFilename, CurrencyHttpClient::GetRawStaticDataResponse())); VERIFY_IS_TRUE(WriteToFileInLocalCacheFolder(CurrencyDataLoaderConstants::AllRatiosDataFilename, CurrencyHttpClient::GetRawAllRatiosDataResponse())); } class CurrencyConverterLoadTests { public: TEST_CLASS(CurrencyConverterLoadTests); TEST_METHOD_SETUP(DeleteCacheFiles) { return DeleteCurrencyCacheFiles(); } TEST_METHOD(LoadFromCache_Fail_NoCacheKey) { RemoveFromLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey); CurrencyDataLoader loader{ nullptr }; bool didLoad = loader.TryLoadDataFromCacheAsync().get(); VERIFY_IS_FALSE(didLoad); VERIFY_IS_FALSE(loader.LoadFinished()); VERIFY_IS_FALSE(loader.LoadedFromCache()); } TEST_METHOD(LoadFromCache_Fail_OlderThanADay) { // Insert 24 hours ago so data is considered stale. // This will cause the load from cache to fail. DateTime now = Utils::GetUniversalSystemTime(); DateTime dayOld; dayOld.UniversalTime = now.UniversalTime - CurrencyDataLoaderConstants::DayDuration - 1; InsertToLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey, dayOld); CurrencyDataLoader loader{ nullptr }; bool didLoad = loader.TryLoadDataFromCacheAsync().get(); VERIFY_IS_FALSE(didLoad); VERIFY_IS_FALSE(loader.LoadFinished()); VERIFY_IS_FALSE(loader.LoadedFromCache()); } TEST_METHOD(LoadFromCache_Fail_StaticDataFileDoesNotExist) { // Insert current time so data is less than a day old. // This will cause the load to continue to attempt to load the file. DateTime now = Utils::GetUniversalSystemTime(); InsertToLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey, now); VERIFY_IS_TRUE(DeleteFileFromLocalCacheFolder(CurrencyDataLoaderConstants::StaticDataFilename)); VERIFY_IS_TRUE(WriteToFileInLocalCacheFolder(CurrencyDataLoaderConstants::AllRatiosDataFilename, CurrencyHttpClient::GetRawAllRatiosDataResponse())); CurrencyDataLoader loader{ nullptr }; bool didLoad = loader.TryLoadDataFromCacheAsync().get(); VERIFY_IS_FALSE(didLoad); VERIFY_IS_FALSE(loader.LoadFinished()); VERIFY_IS_FALSE(loader.LoadedFromCache()); } TEST_METHOD(LoadFromCache_Fail_AllRatiosDataFileDoesNotExist) { // Insert current time so data is less than a day old. // This will cause the load to continue to attempt to load the file. DateTime now = Utils::GetUniversalSystemTime(); InsertToLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey, now); VERIFY_IS_TRUE(WriteToFileInLocalCacheFolder(CurrencyDataLoaderConstants::StaticDataFilename, CurrencyHttpClient::GetRawStaticDataResponse())); VERIFY_IS_TRUE(DeleteFileFromLocalCacheFolder(CurrencyDataLoaderConstants::AllRatiosDataFilename)); CurrencyDataLoader loader{ nullptr }; bool didLoad = loader.TryLoadDataFromCacheAsync().get(); VERIFY_IS_FALSE(didLoad); VERIFY_IS_FALSE(loader.LoadFinished()); VERIFY_IS_FALSE(loader.LoadedFromCache()); } TEST_METHOD(LoadFromCache_Fail_ResponseLanguageChanged) { DateTime now = Utils::GetUniversalSystemTime(); InsertToLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey, now); // Tests always use en-US as response language. Insert a different lang-code to fail the test. InsertToLocalSettings(CurrencyDataLoaderConstants::CacheLangcodeKey, L"ar-SA"); VERIFY_IS_TRUE(WriteToFileInLocalCacheFolder(CurrencyDataLoaderConstants::StaticDataFilename, CurrencyHttpClient::GetRawStaticDataResponse())); VERIFY_IS_TRUE(DeleteFileFromLocalCacheFolder(CurrencyDataLoaderConstants::AllRatiosDataFilename)); CurrencyDataLoader loader{ nullptr }; bool didLoad = loader.TryLoadDataFromCacheAsync().get(); VERIFY_IS_FALSE(didLoad); VERIFY_IS_FALSE(loader.LoadFinished()); VERIFY_IS_FALSE(loader.LoadedFromCache()); } TEST_METHOD(LoadFromCache_Success) { StandardCacheSetup(); CurrencyDataLoader loader{ nullptr }; bool didLoad = loader.TryLoadDataFromCacheAsync().get(); VERIFY_IS_TRUE(didLoad); VERIFY_IS_TRUE(loader.LoadFinished()); VERIFY_IS_TRUE(loader.LoadedFromCache()); } TEST_METHOD(LoadFromWeb_Fail_ClientIsNullptr) { CurrencyDataLoader loader{ nullptr }; bool didLoad = loader.TryLoadDataFromWebAsync().get(); VERIFY_IS_FALSE(didLoad); VERIFY_IS_FALSE(loader.LoadFinished()); VERIFY_IS_FALSE(loader.LoadedFromWeb()); } TEST_METHOD(LoadFromWeb_Fail_WebException) { CurrencyDataLoader loader{ make_unique() }; bool didLoad = loader.TryLoadDataFromWebAsync().get(); VERIFY_IS_FALSE(didLoad); VERIFY_IS_FALSE(loader.LoadFinished()); VERIFY_IS_FALSE(loader.LoadedFromWeb()); } TEST_METHOD(LoadFromWeb_Success) { String^ staticResponse = CurrencyHttpClient::GetRawStaticDataResponse(); String^ allRatiosResponse = CurrencyHttpClient::GetRawAllRatiosDataResponse(); unique_ptr loader = MakeLoaderWithResults(staticResponse, allRatiosResponse); bool didLoad = loader->TryLoadDataFromWebAsync().get(); VERIFY_IS_TRUE(didLoad); VERIFY_IS_TRUE(loader->LoadFinished()); VERIFY_IS_TRUE(loader->LoadedFromWeb()); } TEST_METHOD(Load_Success_LoadedFromCache) { StandardCacheSetup(); CurrencyDataLoader loader{ nullptr }; auto data_loaded_event = task_completion_event(); loader.SetViewModelCallback(make_shared(data_loaded_event)); auto data_loaded_task = create_task(data_loaded_event); loader.LoadData(); data_loaded_task.wait(); VERIFY_IS_TRUE(loader.LoadFinished()); VERIFY_IS_TRUE(loader.LoadedFromCache()); VERIFY_IS_FALSE(loader.LoadedFromWeb()); } TEST_METHOD(Load_Success_LoadedFromWeb) { // Insert 24 hours ago so data is considered stale. // This will cause the load from cache to fail. DateTime now = Utils::GetUniversalSystemTime(); DateTime dayOld; dayOld.UniversalTime = now.UniversalTime - CurrencyDataLoaderConstants::DayDuration - 1; InsertToLocalSettings(CurrencyDataLoaderConstants::CacheTimestampKey, dayOld); String^ staticResponse = CurrencyHttpClient::GetRawStaticDataResponse(); String^ allRatiosResponse = CurrencyHttpClient::GetRawAllRatiosDataResponse(); unique_ptr loader = MakeLoaderWithResults(staticResponse, allRatiosResponse); auto data_loaded_event = task_completion_event(); loader->SetViewModelCallback(make_shared(data_loaded_event)); auto data_loaded_task = create_task(data_loaded_event); loader->LoadData(); data_loaded_task.wait(); VERIFY_IS_TRUE(loader->LoadFinished()); VERIFY_IS_FALSE(loader->LoadedFromCache()); VERIFY_IS_TRUE(loader->LoadedFromWeb()); } }; class CurrencyConverterUnitTests { TEST_CLASS(CurrencyConverterUnitTests); const UCM::Unit GetUnit(const vector& unitList, const wstring& target) { return *find_if(begin(unitList), end(unitList), [&target](const UCM::Unit& u) { return u.abbreviation == target; }); } TEST_METHOD(Loaded_LoadOrderedUnits) { StandardCacheSetup(); CurrencyDataLoader loader{ nullptr }; auto data_loaded_event = task_completion_event(); loader.SetViewModelCallback(make_shared(data_loaded_event)); auto data_loaded_task = create_task(data_loaded_event); loader.LoadData(); data_loaded_task.wait(); VERIFY_IS_TRUE(loader.LoadFinished()); VERIFY_IS_TRUE(loader.LoadedFromCache()); VERIFY_IS_FALSE(loader.LoadedFromWeb()); vector unitList = loader.LoadOrderedUnits(CURRENCY_CATEGORY); VERIFY_ARE_EQUAL(size_t{ 2 }, unitList.size()); const UCM::Unit usdUnit = GetUnit(unitList, L"USD"); const UCM::Unit eurUnit = GetUnit(unitList, L"EUR"); VERIFY_ARE_EQUAL(StringReference(L"United States - Dollar"), ref new String(usdUnit.name.c_str())); VERIFY_ARE_EQUAL(StringReference(L"USD"), ref new String(usdUnit.abbreviation.c_str())); VERIFY_ARE_EQUAL(StringReference(L"Europe - Euro"), ref new String(eurUnit.name.c_str())); VERIFY_ARE_EQUAL(StringReference(L"EUR"), ref new String(eurUnit.abbreviation.c_str())); } TEST_METHOD(Loaded_LoadOrderedRatios) { StandardCacheSetup(); CurrencyDataLoader loader{ nullptr }; auto data_loaded_event = task_completion_event(); loader.SetViewModelCallback(make_shared(data_loaded_event)); auto data_loaded_task = create_task(data_loaded_event); loader.LoadData(); data_loaded_task.wait(); VERIFY_IS_TRUE(loader.LoadFinished()); VERIFY_IS_TRUE(loader.LoadedFromCache()); VERIFY_IS_FALSE(loader.LoadedFromWeb()); vector unitList = loader.LoadOrderedUnits(CURRENCY_CATEGORY); VERIFY_ARE_EQUAL(size_t{ 2 }, unitList.size()); const UCM::Unit usdUnit = GetUnit(unitList, L"USD"); const UCM::Unit eurUnit = GetUnit(unitList, L"EUR"); unordered_map ratios = loader.LoadOrderedRatios(usdUnit); VERIFY_ARE_EQUAL(size_t{ 2 }, ratios.size()); UCM::ConversionData usdRatioData = ratios[usdUnit]; VERIFY_IS_TRUE((std::abs(1.0 - usdRatioData.ratio) < 1e-1)); UCM::ConversionData eurRatioData = ratios[eurUnit]; VERIFY_IS_TRUE((std::abs(0.920503 - eurRatioData.ratio) < 1e-6)); } TEST_METHOD(Loaded_GetCurrencySymbols_Valid) { StandardCacheSetup(); CurrencyDataLoader loader{ nullptr }; auto data_loaded_event = task_completion_event(); loader.SetViewModelCallback(make_shared(data_loaded_event)); auto data_loaded_task = create_task(data_loaded_event); loader.LoadData(); data_loaded_task.wait(); VERIFY_IS_TRUE(loader.LoadFinished()); VERIFY_IS_TRUE(loader.LoadedFromCache()); VERIFY_IS_FALSE(loader.LoadedFromWeb()); vector unitList = loader.LoadOrderedUnits(CURRENCY_CATEGORY); VERIFY_ARE_EQUAL(size_t{ 2 }, unitList.size()); const UCM::Unit usdUnit = GetUnit(unitList, L"USD"); const UCM::Unit eurUnit = GetUnit(unitList, L"EUR"); const pair symbols = loader.GetCurrencySymbols(usdUnit, eurUnit); VERIFY_ARE_EQUAL(ref new String(L"$"), StringReference(symbols.first.c_str())); VERIFY_ARE_EQUAL(ref new String(L"€"), StringReference(symbols.second.c_str())); } TEST_METHOD(Loaded_GetCurrencySymbols_Invalid) { StandardCacheSetup(); CurrencyDataLoader loader{ nullptr }; auto data_loaded_event = task_completion_event(); loader.SetViewModelCallback(make_shared(data_loaded_event)); auto data_loaded_task = create_task(data_loaded_event); loader.LoadData(); data_loaded_task.wait(); VERIFY_IS_TRUE(loader.LoadFinished()); VERIFY_IS_TRUE(loader.LoadedFromCache()); VERIFY_IS_FALSE(loader.LoadedFromWeb()); const UCM::Unit fakeUnit1 = { 1, L"fakeUnit1", L"FUD1", false, false, false }; const UCM::Unit fakeUnit2 = { 2, L"fakeUnit2", L"FUD2", false, false, false }; pair symbols = loader.GetCurrencySymbols(fakeUnit1, fakeUnit2); VERIFY_ARE_EQUAL(ref new String(L""), StringReference(symbols.first.c_str())); VERIFY_ARE_EQUAL(ref new String(L""), StringReference(symbols.second.c_str())); // Verify that when only one unit is valid, both symbols return as empty string. vector unitList = loader.LoadOrderedUnits(CURRENCY_CATEGORY); VERIFY_ARE_EQUAL(size_t{ 2 }, unitList.size()); const UCM::Unit usdUnit = GetUnit(unitList, L"USD"); symbols = loader.GetCurrencySymbols(fakeUnit1, usdUnit); VERIFY_ARE_EQUAL(ref new String(L""), StringReference(symbols.first.c_str())); VERIFY_ARE_EQUAL(ref new String(L""), StringReference(symbols.second.c_str())); symbols = loader.GetCurrencySymbols(usdUnit, fakeUnit1); VERIFY_ARE_EQUAL(ref new String(L""), StringReference(symbols.first.c_str())); VERIFY_ARE_EQUAL(ref new String(L""), StringReference(symbols.second.c_str())); } TEST_METHOD(Loaded_GetCurrencyRatioEquality_Valid) { StandardCacheSetup(); CurrencyDataLoader loader{ nullptr }; auto data_loaded_event = task_completion_event(); loader.SetViewModelCallback(make_shared(data_loaded_event)); auto data_loaded_task = create_task(data_loaded_event); loader.LoadData(); data_loaded_task.wait(); VERIFY_IS_TRUE(loader.LoadFinished()); VERIFY_IS_TRUE(loader.LoadedFromCache()); VERIFY_IS_FALSE(loader.LoadedFromWeb()); vector unitList = loader.LoadOrderedUnits(CURRENCY_CATEGORY); VERIFY_ARE_EQUAL(size_t{ 2 }, unitList.size()); const UCM::Unit usdUnit = GetUnit(unitList, L"USD"); const UCM::Unit eurUnit = GetUnit(unitList, L"EUR"); const pair ratio = loader.GetCurrencyRatioEquality(usdUnit, eurUnit); VERIFY_ARE_EQUAL(ref new String(L"1 USD = 0.9205 EUR"), StringReference(ratio.first.c_str())); VERIFY_ARE_EQUAL(ref new String(L"1 United States Dollar = 0.9205 Europe Euro"), StringReference(ratio.second.c_str())); } TEST_METHOD(Loaded_GetCurrencyRatioEquality_Invalid) { StandardCacheSetup(); CurrencyDataLoader loader{ nullptr }; auto data_loaded_event = task_completion_event(); loader.SetViewModelCallback(make_shared(data_loaded_event)); auto data_loaded_task = create_task(data_loaded_event); loader.LoadData(); data_loaded_task.wait(); VERIFY_IS_TRUE(loader.LoadFinished()); VERIFY_IS_TRUE(loader.LoadedFromCache()); VERIFY_IS_FALSE(loader.LoadedFromWeb()); const UCM::Unit fakeUnit1 = { 1, L"fakeUnit1", L"fakeCountry1", L"FUD1", false, false, false }; const UCM::Unit fakeUnit2 = { 2, L"fakeUnit2", L"fakeCountry2", L"FUD2", false, false, false }; pair ratio = loader.GetCurrencyRatioEquality(fakeUnit1, fakeUnit2); VERIFY_ARE_EQUAL(ref new String(L""), StringReference(ratio.first.c_str())); VERIFY_ARE_EQUAL(ref new String(L""), StringReference(ratio.second.c_str())); // Verify that when only one unit is valid, both symbols return as empty string. vector unitList = loader.LoadOrderedUnits(CURRENCY_CATEGORY); VERIFY_ARE_EQUAL(size_t{ 2 }, unitList.size()); const UCM::Unit usdUnit = GetUnit(unitList, L"USD"); ratio = loader.GetCurrencyRatioEquality(fakeUnit1, usdUnit); VERIFY_ARE_EQUAL(ref new String(L""), StringReference(ratio.first.c_str())); VERIFY_ARE_EQUAL(ref new String(L""), StringReference(ratio.second.c_str())); ratio = loader.GetCurrencyRatioEquality(usdUnit, fakeUnit1); VERIFY_ARE_EQUAL(ref new String(L""), StringReference(ratio.first.c_str())); VERIFY_ARE_EQUAL(ref new String(L""), StringReference(ratio.second.c_str())); } }; }